Compare commits

..

141 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
58 changed files with 3258 additions and 866 deletions

2
.envrc
View file

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

View file

@ -1,12 +1,16 @@
#!/bin/bash
set -e
PLUGINS="$HOME/.local/share/nvim/site/pack/plugins/start"
mkdir -p "$PLUGINS"
wget "https://github.com/neovim/neovim/releases/download/${NVIM_TAG-stable}/nvim.appimage"
version="${NVIM_TAG-stable}"
dl_name="nvim-linux-x86_64.appimage"
# The appimage name changed in v0.10.4
if python -c 'from packaging.version import Version; import sys; sys.exit(not (Version(sys.argv[1]) < Version("v0.10.4")))' "$version" 2>/dev/null; then
dl_name="nvim.appimage"
fi
curl -sL "https://github.com/neovim/neovim/releases/download/${version}/${dl_name}" -o nvim.appimage
chmod +x nvim.appimage
./nvim.appimage --appimage-extract >/dev/null
rm -f nvim.appimage
mkdir -p ~/.local/share/nvim
mv squashfs-root ~/.local/share/nvim/appimage
sudo ln -s "$HOME/.local/share/nvim/appimage/AppRun" /usr/bin/nvim
/usr/bin/nvim --version

View file

@ -4,6 +4,7 @@ on:
push:
branches:
- master
- stevearc-*
pull_request:
branches:
- master
@ -34,7 +35,7 @@ jobs:
uses: JohnnyMorganz/stylua-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: v0.20.0
version: v2.0.2
args: --check lua tests
typecheck:
@ -52,7 +53,8 @@ jobs:
include:
- nvim_tag: v0.8.3
- nvim_tag: v0.9.4
- nvim_tag: v0.10.0
- nvim_tag: v0.10.4
- nvim_tag: v0.11.0
name: Run tests
runs-on: ubuntu-22.04

7
.gitignore vendored
View file

@ -6,6 +6,9 @@ luac.out
*.zip
*.tar.gz
# python bytecode
__pycache__
# Object files
*.o
*.os
@ -41,6 +44,10 @@ luac.out
.direnv/
.testenv/
venv/
doc/tags
scripts/nvim_doc_tools
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,107 @@
# 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)

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
doc: scripts/nvim_doc_tools
python scripts/main.py generate
python scripts/main.py lint
venv:
python3 -m venv venv
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:
./run_tests.sh
## lint: run linters and LuaLS typechecking
.PHONY: lint
lint: scripts/nvim-typecheck-action fastlint
./scripts/nvim-typecheck-action/typecheck.sh --workdir scripts/nvim-typecheck-action lua
fastlint: scripts/nvim_doc_tools
python scripts/main.py lint
## fastlint: run only fast linters
.PHONY: fastlint
fastlint: scripts/nvim_doc_tools venv
venv/bin/python scripts/main.py lint
luacheck lua tests --formatter plain
stylua --check lua tests
## profile: use LuaJIT profiler to profile the plugin
.PHONY: profile
profile: scripts/benchmark.nvim
nvim --clean -u perf/bootstrap.lua -c 'lua jit_profile()'
## flame_profile: create a trace in the chrome profiler format
.PHONY: flame_profile
flame_profile: scripts/benchmark.nvim
nvim --clean -u perf/bootstrap.lua -c 'lua flame_profile()'
## benchmark: benchmark performance opening directory with many files
.PHONY: benchmark
benchmark: scripts/benchmark.nvim
nvim --clean -u perf/bootstrap.lua -c 'lua benchmark()'
@cat perf/tmp/benchmark.txt
scripts/nvim_doc_tools:
git clone https://github.com/stevearc/nvim_doc_tools scripts/nvim_doc_tools
scripts/nvim-typecheck-action:
git clone https://github.com/stevearc/nvim-typecheck-action scripts/nvim-typecheck-action
scripts/benchmark.nvim:
git clone https://github.com/stevearc/benchmark.nvim scripts/benchmark.nvim
## clean: reset the repository to a clean state
.PHONY: clean
clean:
rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action
rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv perf/tmp profile.json

119
README.md
View file

@ -12,6 +12,7 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94
- [Options](#options)
- [Adapters](#adapters)
- [Recipes](#recipes)
- [Third-party extensions](#third-party-extensions)
- [API](#api)
- [FAQ](#faq)
@ -21,7 +22,7 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94
- Neovim 0.8+
- Icon provider plugin (optional)
- [mini.icons](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-icons.md) for file and folder icons
- [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
@ -34,10 +35,14 @@ oil.nvim supports all the usual plugin managers
```lua
{
'stevearc/oil.nvim',
---@module 'oil'
---@type oil.SetupOpts
opts = {},
-- Optional dependencies
dependencies = { { "echasnovski/mini.icons", opts = {} } },
-- dependencies = { "nvim-tree/nvim-web-devicons" }, -- use if prefer 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,
}
```
@ -168,6 +173,8 @@ require("oil").setup({
-- Note that the cleanup process only starts when none of the oil buffers are currently displayed
cleanup_delay_ms = 2000,
lsp_file_methods = {
-- Enable or disable LSP file operations
enabled = true,
-- Time to wait for LSP file operations to complete before skipping
timeout_ms = 1000,
-- Set to true to autosave buffers that are updated with LSP willRenameFiles
@ -186,22 +193,22 @@ require("oil").setup({
-- Set to `false` to remove a keymap
-- See :help oil-actions for a list of all available actions
keymaps = {
["g?"] = "actions.show_help",
["g?"] = { "actions.show_help", mode = "n" },
["<CR>"] = "actions.select",
["<C-s>"] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" },
["<C-h>"] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" },
["<C-t>"] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" },
["<C-s>"] = { "actions.select", opts = { vertical = true } },
["<C-h>"] = { "actions.select", opts = { horizontal = true } },
["<C-t>"] = { "actions.select", opts = { tab = true } },
["<C-p>"] = "actions.preview",
["<C-c>"] = "actions.close",
["<C-c>"] = { "actions.close", mode = "n" },
["<C-l>"] = "actions.refresh",
["-"] = "actions.parent",
["_"] = "actions.open_cwd",
["`"] = "actions.cd",
["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" },
["gs"] = "actions.change_sort",
["-"] = { "actions.parent", mode = "n" },
["_"] = { "actions.open_cwd", mode = "n" },
["`"] = { "actions.cd", mode = "n" },
["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
["gs"] = { "actions.change_sort", mode = "n" },
["gx"] = "actions.open_external",
["g."] = "actions.toggle_hidden",
["g\\"] = "actions.toggle_trash",
["g."] = { "actions.toggle_hidden", mode = "n" },
["g\\"] = { "actions.toggle_trash", mode = "n" },
},
-- Set to false to disable all of the above keymaps
use_default_keymaps = true,
@ -210,15 +217,16 @@ require("oil").setup({
show_hidden = false,
-- This function defines what is considered a "hidden" file
is_hidden_file = function(name, bufnr)
return vim.startswith(name, ".")
local m = name:match("^%.")
return m ~= nil
end,
-- This function defines what will never be shown, even when `show_hidden` is set
is_always_hidden = function(name, bufnr)
return false
end,
-- Sort file names in a more intuitive order for humans. Is less performant,
-- so you may want to set to false if you work with large directories.
natural_order = true,
-- Sort file 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 = {
@ -227,9 +235,15 @@ require("oil").setup({
{ "type", "asc" },
{ "name", "asc" },
},
-- Customize the highlight group for the file name
highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan)
return nil
end,
},
-- Extra arguments to pass to SCP when moving/copying files over SSH
extra_scp_args = {},
-- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3
extra_s3_args = {},
-- EXPERIMENTAL support for performing file operations with git
git = {
-- Return true to automatically git add/mv/rm files
@ -247,12 +261,15 @@ require("oil").setup({
float = {
-- Padding around the floating window
padding = 2,
-- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
max_width = 0,
max_height = 0,
border = "rounded",
border = nil,
win_options = {
winblend = 0,
},
-- optionally override the oil buffers window title with custom function: fun(winid: integer): string
get_win_title = nil,
-- preview_split: Split direction: "auto", "left", "right", "above", "below".
preview_split = "auto",
-- This is the config that will be passed to nvim_open_win.
@ -261,8 +278,21 @@ require("oil").setup({
return conf
end,
},
-- Configuration for the actions floating preview window
preview = {
-- Configuration for the file preview window
preview_win = {
-- Whether the preview window is automatically updated when the cursor is moved
update_on_cursor_moved = true,
-- How to open the preview window "load"|"scratch"|"fast_scratch"
preview_method = "fast_scratch",
-- A function that returns true to disable preview on a file e.g. to avoid lag
disable_preview = function(filename)
return false
end,
-- Window-local options to use for preview window buffers
win_options = {},
},
-- Configuration for the floating action confirmation window
confirmation = {
-- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
-- min_width and max_width can be a single value or a list of mixed integer/float types.
-- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total"
@ -279,12 +309,10 @@ require("oil").setup({
min_height = { 5, 0.1 },
-- optionally define an integer/float for the exact height of the preview window
height = nil,
border = "rounded",
border = nil,
win_options = {
winblend = 0,
},
-- Whether the preview window is automatically updated when the cursor is moved
update_on_cursor_moved = true,
},
-- Configuration for the floating progress window
progress = {
@ -294,7 +322,7 @@ require("oil").setup({
max_height = { 10, 0.9 },
min_height = { 5, 0.1 },
height = nil,
border = "rounded",
border = nil,
minimized_border = "none",
win_options = {
winblend = 0,
@ -302,11 +330,11 @@ require("oil").setup({
},
-- Configuration for the floating SSH window
ssh = {
border = "rounded",
border = nil,
},
-- Configuration for the floating keymaps help window
keymaps_help = {
border = "rounded",
border = nil,
},
})
```
@ -329,10 +357,29 @@ This may look familiar. In fact, this is the same url format that netrw uses.
Note that at the moment the ssh adapter does not support Windows machines, and it requires the server to have a `/bin/sh` binary as well as standard unix commands (`ls`, `rm`, `mv`, `mkdir`, `chmod`, `cp`, `touch`, `ln`, `echo`).
### S3
This adapter allows you to browse files stored in aws s3. To use it, make sure `aws` is setup correctly and then simply open a buffer using the following name template:
```
nvim oil-s3://[bucket]/[path]
```
Note that older versions of Neovim don't support numbers in the url, so for Neovim 0.11 and older the url starts with `oil-sss`.
## Recipes
- [Toggle file detail view](doc/recipes.md#toggle-file-detail-view)
- [Hide gitignored files](doc/recipes.md#hide-gitignored-files)
- [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
@ -346,11 +393,11 @@ Note that at the moment the ssh adapter does not support Windows machines, and i
- [set_is_hidden_file(is_hidden_file)](doc/api.md#set_is_hidden_fileis_hidden_file)
- [toggle_hidden()](doc/api.md#toggle_hidden)
- [get_current_dir(bufnr)](doc/api.md#get_current_dirbufnr)
- [open_float(dir)](doc/api.md#open_floatdir)
- [toggle_float(dir)](doc/api.md#toggle_floatdir)
- [open(dir)](doc/api.md#opendir)
- [close()](doc/api.md#close)
- [open_preview(opts)](doc/api.md#open_previewopts)
- [open_float(dir, opts, cb)](doc/api.md#open_floatdir-opts-cb)
- [toggle_float(dir, opts, cb)](doc/api.md#toggle_floatdir-opts-cb)
- [open(dir, opts, cb)](doc/api.md#opendir-opts-cb)
- [close(opts)](doc/api.md#closeopts)
- [open_preview(opts, callback)](doc/api.md#open_previewopts-callback)
- [select(opts, callback)](doc/api.md#selectopts-callback)
- [save(opts, cb)](doc/api.md#saveopts-cb)
- [setup(opts)](doc/api.md#setupopts)
@ -374,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)
- 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
@ -390,7 +437,7 @@ If you don't need those features specifically, check out the alternatives listed
**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.
- [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).

View file

@ -10,11 +10,11 @@
- [set_is_hidden_file(is_hidden_file)](#set_is_hidden_fileis_hidden_file)
- [toggle_hidden()](#toggle_hidden)
- [get_current_dir(bufnr)](#get_current_dirbufnr)
- [open_float(dir)](#open_floatdir)
- [toggle_float(dir)](#toggle_floatdir)
- [open(dir)](#opendir)
- [close()](#close)
- [open_preview(opts)](#open_previewopts)
- [open_float(dir, opts, cb)](#open_floatdir-opts-cb)
- [toggle_float(dir, opts, cb)](#toggle_floatdir-opts-cb)
- [open(dir, opts, cb)](#opendir-opts-cb)
- [close(opts)](#closeopts)
- [open_preview(opts, callback)](#open_previewopts-callback)
- [select(opts, callback)](#selectopts-callback)
- [save(opts, cb)](#saveopts-cb)
- [setup(opts)](#setupopts)
@ -73,9 +73,9 @@ require("oil").set_sort({ { "type", "asc" }, { "size", "desc" } })
`set_is_hidden_file(is_hidden_file)` \
Change how oil determines if the file is hidden
| Param | Type | Desc |
| -------------- | ----------------------------------------------------- | -------------------------------------------- |
| is_hidden_file | `fun(filename: string, bufnr: nil\|integer): boolean` | Return true if the file/dir should be hidden |
| Param | Type | Desc |
| -------------- | ------------------------------------------------ | -------------------------------------------- |
| is_hidden_file | `fun(filename: string, bufnr: integer): boolean` | Return true if the file/dir should be hidden |
## toggle_hidden()
@ -92,76 +92,100 @@ Get the current directory
| ----- | -------------- | ---- |
| 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
| 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 |
| Param | Type | Desc |
| ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file |
| opts | `nil\|oil.OpenOpts` | |
| >preview | `nil\|oil.OpenPreviewOpts` | When present, open the preview window after opening oil |
| >>vertical | `nil\|boolean` | Open the buffer in a vertical split |
| >>horizontal | `nil\|boolean` | Open the buffer in a horizontal split |
| >>split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier |
| cb | `nil\|fun()` | Called after the oil buffer is ready |
## 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
| 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 |
| Param | Type | Desc |
| ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file |
| opts | `nil\|oil.OpenOpts` | |
| >preview | `nil\|oil.OpenPreviewOpts` | When present, open the preview window after opening oil |
| >>vertical | `nil\|boolean` | Open the buffer in a vertical split |
| >>horizontal | `nil\|boolean` | Open the buffer in a horizontal split |
| >>split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier |
| cb | `nil\|fun()` | Called after the oil buffer is ready |
## open(dir)
## open(dir, opts, cb)
`open(dir)` \
`open(dir, opts, cb)` \
Open oil browser for a directory
| Param | Type | Desc |
| ----- | ------------- | ------------------------------------------------------------------------------------------- |
| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file |
| Param | Type | Desc |
| ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file |
| opts | `nil\|oil.OpenOpts` | |
| >preview | `nil\|oil.OpenPreviewOpts` | When present, open the preview window after opening oil |
| >>vertical | `nil\|boolean` | Open the buffer in a vertical split |
| >>horizontal | `nil\|boolean` | Open the buffer in a horizontal split |
| >>split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier |
| cb | `nil\|fun()` | Called after the oil buffer is ready |
## close()
## close(opts)
`close()` \
`close(opts)` \
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)
## open_preview(opts, callback)
`open_preview(opts)` \
`open_preview(opts, callback)` \
Preview the entry under the cursor in a split
| Param | Type | Desc | |
| ----- | ------------ | -------------------------------------------------- | ------------------------------------- |
| opts | `nil\|table` | | |
| | vertical | `boolean` | Open the buffer in a vertical split |
| | horizontal | `boolean` | Open the buffer in a horizontal split |
| | split | `"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier |
| 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 the entry under the cursor
| Param | Type | Desc | |
| -------- | ---------------------------- | ------------------------------------------------------- | ---------------------------------------------------- |
| opts | `nil\|oil.SelectOpts` | | |
| | vertical | `nil\|boolean` | Open the buffer in a vertical split |
| | horizontal | `nil\|boolean` | Open the buffer in a horizontal split |
| | split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier |
| | tab | `nil\|boolean` | Open the buffer in a new tab |
| | close | `nil\|boolean` | Close the original oil buffer once selection is made |
| callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened | |
| Param | Type | Desc |
| ----------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| opts | `nil\|oil.SelectOpts` | |
| >vertical | `nil\|boolean` | Open the buffer in a vertical split |
| >horizontal | `nil\|boolean` | Open the buffer in a horizontal split |
| >split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier |
| >tab | `nil\|boolean` | Open the buffer in a new tab |
| >close | `nil\|boolean` | Close the original oil buffer once selection is made |
| >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 |
## save(opts, cb)
`save(opts, cb)` \
Save all changes
| Param | Type | Desc | |
| ----- | ---------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------- |
| opts | `nil\|table` | | |
| | confirm | `nil\|boolean` | Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil |
| cb | `nil\|fun(err: nil\|string)` | Called when mutations complete. | |
| Param | Type | Desc |
| -------- | ---------------------------- | ------------------------------------------------------------------------------------------- |
| opts | `nil\|table` | |
| >confirm | `nil\|boolean` | Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil |
| cb | `nil\|fun(err: nil\|string)` | Called when mutations complete. |
**Note:**
<pre>

View file

@ -55,6 +55,8 @@ CONFIG *oil-confi
-- Note that the cleanup process only starts when none of the oil buffers are currently displayed
cleanup_delay_ms = 2000,
lsp_file_methods = {
-- Enable or disable LSP file operations
enabled = true,
-- Time to wait for LSP file operations to complete before skipping
timeout_ms = 1000,
-- Set to true to autosave buffers that are updated with LSP willRenameFiles
@ -73,22 +75,22 @@ CONFIG *oil-confi
-- Set to `false` to remove a keymap
-- See :help oil-actions for a list of all available actions
keymaps = {
["g?"] = "actions.show_help",
["g?"] = { "actions.show_help", mode = "n" },
["<CR>"] = "actions.select",
["<C-s>"] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" },
["<C-h>"] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" },
["<C-t>"] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" },
["<C-s>"] = { "actions.select", opts = { vertical = true } },
["<C-h>"] = { "actions.select", opts = { horizontal = true } },
["<C-t>"] = { "actions.select", opts = { tab = true } },
["<C-p>"] = "actions.preview",
["<C-c>"] = "actions.close",
["<C-c>"] = { "actions.close", mode = "n" },
["<C-l>"] = "actions.refresh",
["-"] = "actions.parent",
["_"] = "actions.open_cwd",
["`"] = "actions.cd",
["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" },
["gs"] = "actions.change_sort",
["-"] = { "actions.parent", mode = "n" },
["_"] = { "actions.open_cwd", mode = "n" },
["`"] = { "actions.cd", mode = "n" },
["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
["gs"] = { "actions.change_sort", mode = "n" },
["gx"] = "actions.open_external",
["g."] = "actions.toggle_hidden",
["g\\"] = "actions.toggle_trash",
["g."] = { "actions.toggle_hidden", mode = "n" },
["g\\"] = { "actions.toggle_trash", mode = "n" },
},
-- Set to false to disable all of the above keymaps
use_default_keymaps = true,
@ -97,15 +99,16 @@ CONFIG *oil-confi
show_hidden = false,
-- This function defines what is considered a "hidden" file
is_hidden_file = function(name, bufnr)
return vim.startswith(name, ".")
local m = name:match("^%.")
return m ~= nil
end,
-- This function defines what will never be shown, even when `show_hidden` is set
is_always_hidden = function(name, bufnr)
return false
end,
-- Sort file names in a more intuitive order for humans. Is less performant,
-- so you may want to set to false if you work with large directories.
natural_order = true,
-- Sort file 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 = {
@ -114,9 +117,15 @@ CONFIG *oil-confi
{ "type", "asc" },
{ "name", "asc" },
},
-- Customize the highlight group for the file name
highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan)
return nil
end,
},
-- Extra arguments to pass to SCP when moving/copying files over SSH
extra_scp_args = {},
-- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3
extra_s3_args = {},
-- EXPERIMENTAL support for performing file operations with git
git = {
-- Return true to automatically git add/mv/rm files
@ -134,12 +143,15 @@ CONFIG *oil-confi
float = {
-- Padding around the floating window
padding = 2,
-- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
max_width = 0,
max_height = 0,
border = "rounded",
border = nil,
win_options = {
winblend = 0,
},
-- optionally override the oil buffers window title with custom function: fun(winid: integer): string
get_win_title = nil,
-- preview_split: Split direction: "auto", "left", "right", "above", "below".
preview_split = "auto",
-- This is the config that will be passed to nvim_open_win.
@ -148,8 +160,21 @@ CONFIG *oil-confi
return conf
end,
},
-- Configuration for the actions floating preview window
preview = {
-- Configuration for the file preview window
preview_win = {
-- Whether the preview window is automatically updated when the cursor is moved
update_on_cursor_moved = true,
-- How to open the preview window "load"|"scratch"|"fast_scratch"
preview_method = "fast_scratch",
-- A function that returns true to disable preview on a file e.g. to avoid lag
disable_preview = function(filename)
return false
end,
-- Window-local options to use for preview window buffers
win_options = {},
},
-- Configuration for the floating action confirmation window
confirmation = {
-- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
-- min_width and max_width can be a single value or a list of mixed integer/float types.
-- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total"
@ -166,12 +191,10 @@ CONFIG *oil-confi
min_height = { 5, 0.1 },
-- optionally define an integer/float for the exact height of the preview window
height = nil,
border = "rounded",
border = nil,
win_options = {
winblend = 0,
},
-- Whether the preview window is automatically updated when the cursor is moved
update_on_cursor_moved = true,
},
-- Configuration for the floating progress window
progress = {
@ -181,7 +204,7 @@ CONFIG *oil-confi
max_height = { 10, 0.9 },
min_height = { 5, 0.1 },
height = nil,
border = "rounded",
border = nil,
minimized_border = "none",
win_options = {
winblend = 0,
@ -189,11 +212,11 @@ CONFIG *oil-confi
},
-- Configuration for the floating SSH window
ssh = {
border = "rounded",
border = nil,
},
-- Configuration for the floating keymaps help window
keymaps_help = {
border = "rounded",
border = nil,
},
})
<
@ -270,8 +293,8 @@ set_is_hidden_file({is_hidden_file}) *oil.set_is_hidden_fil
Change how oil determines if the file is hidden
Parameters:
{is_hidden_file} `fun(filename: string, bufnr: nil|integer): boolean` Retu
rn 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() *oil.toggle_hidden*
Toggle hidden files and directories
@ -283,40 +306,70 @@ get_current_dir({bufnr}): nil|string *oil.get_current_di
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
Parameters:
{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"` 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
Parameters:
{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"` 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
Parameters:
{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"` 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
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}) *oil.open_preview*
open_preview({opts}, {callback}) *oil.open_preview*
Preview the entry under the cursor in a split
Parameters:
{opts} `nil|table`
{vertical} `boolean` Open the buffer in a vertical split
{horizontal} `boolean` Open the buffer in a horizontal split
{split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split
{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 the entry under the cursor
@ -330,6 +383,10 @@ select({opts}, {callback}) *oil.selec
{tab} `nil|boolean` Open the buffer in a new tab
{close} `nil|boolean` Close the original oil buffer once
selection is made
{handle_buffer_callback} `nil|fun(buf_id: integer)` If defined, all
other buffer related options here would be ignored. This
callback allows you to take over the process of opening
the buffer yourself.
{callback} `nil|fun(err: nil|string)` Called once all entries have been
opened
@ -365,6 +422,7 @@ type *column-typ
Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{icons} `table<string, string>` Mapping of entry type to icon
icon *column-icon*
@ -374,6 +432,7 @@ icon *column-ico
Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{default_file} `string` Fallback icon for files when nvim-web-devicons
returns nil
{directory} `string` Icon for directories
@ -381,13 +440,14 @@ icon *column-ico
the icon
size *column-size*
Adapters: files, ssh
Adapters: files, ssh, s3
Sortable: this column can be used in view_props.sort
The size of the file
Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
permissions *column-permissions*
Adapters: files, ssh
@ -397,6 +457,7 @@ permissions *column-permission
Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
ctime *column-ctime*
Adapters: files
@ -406,6 +467,7 @@ ctime *column-ctim
Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{format} `string` Format string (see :help strftime)
mtime *column-mtime*
@ -416,6 +478,7 @@ mtime *column-mtim
Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{format} `string` Format string (see :help strftime)
atime *column-atime*
@ -426,16 +489,18 @@ atime *column-atim
Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{format} `string` Format string (see :help strftime)
birthtime *column-birthtime*
Adapters: files
Adapters: files, s3
Sortable: this column can be used in view_props.sort
The time the file was created
Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{format} `string` Format string (see :help strftime)
--------------------------------------------------------------------------------
@ -499,6 +564,12 @@ change_sort *actions.change_sor
close *actions.close*
Close oil and restore original buffer
Parameters:
{exit_if_last_buf} `boolean` Exit vim if oil is closed as the last buffer
copy_to_system_clipboard *actions.copy_to_system_clipboard*
Copy the entry under the cursor to the system clipboard
open_cmdline *actions.open_cmdline*
Open vim cmdline with current entry as an argument
@ -519,13 +590,31 @@ open_terminal *actions.open_termina
parent *actions.parent*
Navigate to the parent path
paste_from_system_clipboard *actions.paste_from_system_clipboard*
Paste the system clipboard into the current oil directory
Parameters:
{delete_original} `boolean` Delete the original file after copying
preview *actions.preview*
Open the entry under the cursor in a preview window, or close the preview
window if already open
Parameters:
{horizontal} `boolean` Open the buffer in a horizontal split
{split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split
modifier
{vertical} `boolean` Open the buffer in a vertical split
preview_scroll_down *actions.preview_scroll_down*
Scroll down in the preview window
preview_scroll_left *actions.preview_scroll_left*
Scroll left in the preview window
preview_scroll_right *actions.preview_scroll_right*
Scroll right in the preview window
preview_scroll_up *actions.preview_scroll_up*
Scroll up in the preview window
@ -555,6 +644,9 @@ send_to_qflist *actions.send_to_qflis
Parameters:
{action} `"r"|"a"` Replace or add to current quickfix list (see
|setqflist-action|)
{only_matching_search} `boolean` Whether to only add the files that
matches the last search. This option only applies when search
highlighting is active
{target} `"qflist"|"loclist"` The target list to send files to
show_help *actions.show_help*
@ -576,24 +668,57 @@ yank_entry *actions.yank_entr
--------------------------------------------------------------------------------
HIGHLIGHTS *oil-highlights*
OilEmpty *hl-OilEmpty*
Empty column values
OilHidden *hl-OilHidden*
Hidden entry in an oil buffer
OilDir *hl-OilDir*
Directory names in an oil buffer
OilDirHidden *hl-OilDirHidden*
Hidden directory names in an oil buffer
OilDirIcon *hl-OilDirIcon*
Icon for directories
OilSocket *hl-OilSocket*
Socket files in an oil buffer
OilSocketHidden *hl-OilSocketHidden*
Hidden socket files in an oil buffer
OilLink *hl-OilLink*
Soft links in an oil buffer
OilOrphanLink *hl-OilOrphanLink*
Orphaned soft links in an oil buffer
OilLinkHidden *hl-OilLinkHidden*
Hidden soft links in an oil buffer
OilOrphanLinkHidden *hl-OilOrphanLinkHidden*
Hidden orphaned soft links in an oil buffer
OilLinkTarget *hl-OilLinkTarget*
The target of a soft link
OilOrphanLinkTarget *hl-OilOrphanLinkTarget*
The target of an orphaned soft link
OilLinkTargetHidden *hl-OilLinkTargetHidden*
The target of a hidden soft link
OilOrphanLinkTargetHidden *hl-OilOrphanLinkTargetHidden*
The target of an hidden orphaned soft link
OilFile *hl-OilFile*
Normal files in an oil buffer
OilFileHidden *hl-OilFileHidden*
Hidden normal files in an oil buffer
OilCreate *hl-OilCreate*
Create action in the oil preview window
@ -632,13 +757,13 @@ of being permanently deleted. You can browse the trash for a directory using
the `toggle_trash` action (bound to `g\` by default). You can view all files
in the trash with `:Oil --trash /`.
To restore files, simply delete them from the trash and put them in the desired
destination, the same as any other file operation. If you delete files from the
trash they will be permanently deleted (purged).
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/trashspec-1.0.html
https://specifications.freedesktop.org/trash-spec/1.0/
All features should work.
Mac:
@ -647,7 +772,7 @@ Mac:
(instead of being able to see files that were trashed from a directory).
Windows:
Oil does not yet support the Windows trash. PRs are welcome!
Oil supports the Windows Recycle Bin. All features should work.
================================================================================
vim:tw=80:ts=2:ft=help:norl:syntax=help:

View file

@ -5,7 +5,8 @@ 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)
- [Hide gitignored files](#hide-gitignored-files)
- [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 -->
@ -30,47 +31,96 @@ require("oil").setup({
})
```
## Hide gitignored files
## Show CWD in the winbar
```lua
local git_ignored = setmetatable({}, {
__index = function(self, key)
local proc = vim.system(
{ "git", "ls-files", "--ignored", "--exclude-standard", "--others", "--directory" },
{
-- 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),
}
)
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("/$", "")
table.insert(ret, line)
end
end
rawset(self, key, ret)
return ret
end,
})
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, _)
-- dotfiles are always considered hidden
if vim.startswith(name, ".") then
return true
end
local dir = require("oil").get_current_dir()
-- if no local directory (e.g. for ssh connections), always show
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 false
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
-- Check if file is gitignored
return vim.list_contains(git_ignored[dir], name)
end,
},
})

View file

@ -69,7 +69,21 @@ M.select_tab = {
M.preview = {
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()
if not entry then
vim.notify("Could not find entry under cursor", vim.log.levels.ERROR)
@ -88,7 +102,7 @@ M.preview = {
return
end
end
oil.open_preview()
oil.open_preview(opts)
end,
}
@ -122,6 +136,30 @@ M.preview_scroll_up = {
end,
}
M.preview_scroll_left = {
desc = "Scroll left in the preview window",
callback = function()
local winid = util.get_preview_win()
if winid then
vim.api.nvim_win_call(winid, function()
vim.cmd.normal({ "zH", bang = true })
end)
end
end,
}
M.preview_scroll_right = {
desc = "Scroll right in the preview window",
callback = function()
local winid = util.get_preview_win()
if winid then
vim.api.nvim_win_call(winid, function()
vim.cmd.normal({ "zL", bang = true })
end)
end
end,
}
M.parent = {
desc = "Navigate to the parent path",
callback = oil.open,
@ -129,7 +167,16 @@ M.parent = {
M.close = {
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
@ -206,13 +253,24 @@ M.open_terminal = {
assert(dir, "Oil buffer with files adapter must have current directory")
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_set_current_buf(bufnr)
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
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_set_current_buf(bufnr)
local url = require("oil.adapters.ssh").parse_url(bufname)
local cmd = require("oil.adapters.ssh.connection").create_ssh_command(url)
local term_id = vim.fn.termopen(cmd)
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
vim.api.nvim_chan_send(term_id, string.format("cd %s\n", url.path))
end
@ -352,7 +410,11 @@ M.yank_entry = {
if not entry or not dir then
return
end
local path = dir .. entry.name
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
@ -391,6 +453,26 @@ M.copy_entry_filename = {
end,
}
M.copy_to_system_clipboard = {
desc = "Copy the entry under the cursor to the system clipboard",
callback = function()
require("oil.clipboard").copy_to_system_clipboard()
end,
}
M.paste_from_system_clipboard = {
desc = "Paste the system clipboard into the current oil directory",
callback = function(opts)
require("oil.clipboard").paste_from_system_clipboard(opts and opts.delete_original)
end,
parameters = {
delete_original = {
type = "boolean",
desc = "Delete the original file after copying",
},
},
}
M.open_cmdline_dir = {
desc = "Open vim cmdline with current directory as an argument",
deprecated = true,
@ -477,10 +559,12 @@ M.send_to_qflist = {
opts = vim.tbl_deep_extend("keep", opts or {}, {
target = "qflist",
action = "r",
only_matching_search = false,
})
util.send_to_quickfix({
target = opts.target,
action = opts.action,
only_matching_search = opts.only_matching_search,
})
end,
parameters = {
@ -492,6 +576,10 @@ M.send_to_qflist = {
type = '"r"|"a"',
desc = "Replace or add to current quickfix list (see |setqflist-action|)",
},
only_matching_search = {
type = "boolean",
desc = "Whether to only add the files that matches the last search. This option only applies when search highlighting is active",
},
},
}

View file

@ -4,14 +4,15 @@ local config = require("oil.config")
local constants = require("oil.constants")
local fs = require("oil.fs")
local git = require("oil.git")
local log = require("oil.log")
local permissions = require("oil.adapters.files.permissions")
local trash = require("oil.adapters.files.trash")
local util = require("oil.util")
local uv = vim.uv or vim.loop
local M = {}
local FIELD_NAME = constants.FIELD_NAME
local FIELD_TYPE = constants.FIELD_TYPE
local FIELD_META = constants.FIELD_META
local function read_link_data(path, cb)
@ -34,6 +35,9 @@ local function read_link_data(path, cb)
)
end
---@class (exact) oil.FilesAdapter: oil.Adapter
---@field to_short_os_path fun(path: string, entry_type: nil|oil.EntryType): string
---@param path string
---@param entry_type nil|oil.EntryType
---@return string
@ -47,21 +51,12 @@ end
local file_columns = {}
local fs_stat_meta_fields = {
stat = function(parent_url, entry, cb)
local _, path = util.parse_url(parent_url)
assert(path)
local dir = fs.posix_to_os_path(path .. entry[FIELD_NAME])
uv.fs_stat(dir, cb)
end,
}
file_columns.size = {
meta_fields = fs_stat_meta_fields,
require_stat = true,
render = function(entry, conf)
local meta = entry[FIELD_META]
local stat = meta.stat
local stat = meta and meta.stat
if not stat then
return columns.EMPTY
end
@ -78,7 +73,7 @@ file_columns.size = {
get_sort_value = function(entry)
local meta = entry[FIELD_META]
local stat = meta.stat
local stat = meta and meta.stat
if stat then
return stat.size
else
@ -94,11 +89,11 @@ file_columns.size = {
-- TODO support file permissions on windows
if not fs.is_windows then
file_columns.permissions = {
meta_fields = fs_stat_meta_fields,
require_stat = true,
render = function(entry, conf)
local meta = entry[FIELD_META]
local stat = meta.stat
local stat = meta and meta.stat
if not stat then
return columns.EMPTY
end
@ -111,7 +106,7 @@ if not fs.is_windows then
compare = function(entry, parsed_value)
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 old_mode = bit.band(meta.stat.mode, mask)
if parsed_value ~= old_mode then
@ -157,11 +152,11 @@ end)
for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
file_columns[time_key] = {
meta_fields = fs_stat_meta_fields,
require_stat = true,
render = function(entry, conf)
local meta = entry[FIELD_META]
local stat = meta.stat
local stat = meta and meta.stat
if not stat then
return columns.EMPTY
end
@ -184,7 +179,20 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
local fmt = conf and conf.format
local pattern
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
pattern = "%S+%s+%d+%s+%d%d:?%d%d"
end
@ -193,7 +201,7 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
get_sort_value = function(entry)
local meta = entry[FIELD_META]
local stat = meta.stat
local stat = meta and meta.stat
if stat then
return stat[time_key].sec
else
@ -203,6 +211,20 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
}
end
---@param column_defs table[]
---@return boolean
local function columns_require_stat(column_defs)
for _, def in ipairs(column_defs) do
local name = util.split_config(def)
local column = M.get_column(name)
---@diagnostic disable-next-line: undefined-field We only put this on the files adapter columns
if column and column.require_stat then
return true
end
end
return false
end
---@param name string
---@return nil|oil.ColumnDefinition
M.get_column = function(name)
@ -254,7 +276,7 @@ M.normalize_url = function(url, callback)
local norm_path = util.addslash(fs.os_to_posix_path(realpath))
callback(scheme .. norm_path)
else
callback(realpath)
callback(vim.fn.fnamemodify(realpath, ":."))
end
end)
)
@ -280,11 +302,102 @@ M.get_entry_path = function(url, entry, cb)
end
end
---@param parent_dir string
---@param entry oil.InternalEntry
---@param require_stat boolean
---@param cb fun(err?: string)
local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
local entry_path = fs.posix_to_os_path(parent_dir .. entry[FIELD_NAME])
local meta = entry[FIELD_META]
if not meta then
meta = {}
entry[FIELD_META] = meta
end
-- Sometimes fs_readdir entries don't have a type, so we need to stat them.
-- See https://github.com/stevearc/oil.nvim/issues/543
if not require_stat and not entry[FIELD_TYPE] then
require_stat = true
end
-- Make sure we always get fs_stat info for links
if entry[FIELD_TYPE] == "link" then
read_link_data(entry_path, function(link_err, link, link_stat)
if link_err then
log.warn("Error reading link data %s: %s", entry_path, link_err)
return cb()
end
meta.link = link
if link_stat then
-- Use the fstat of the linked file as the stat for the link
meta.link_stat = link_stat
meta.stat = link_stat
elseif require_stat then
-- The link is broken, so let's use the stat of the link itself
uv.fs_lstat(entry_path, function(stat_err, stat)
if stat_err then
log.warn("Error lstat link file %s: %s", entry_path, stat_err)
return cb()
end
meta.stat = stat
cb()
end)
return
end
cb()
end)
elseif require_stat then
uv.fs_stat(entry_path, function(stat_err, stat)
if stat_err then
log.warn("Error stat file %s: %s", entry_path, stat_err)
return cb()
end
assert(stat)
entry[FIELD_TYPE] = stat.type
meta.stat = stat
cb()
end)
else
cb()
end
end
-- On windows, sometimes the entry type from fs_readdir is "link" but the actual type is not.
-- See https://github.com/stevearc/oil.nvim/issues/535
if fs.is_windows then
local old_fetch_metadata = fetch_entry_metadata
fetch_entry_metadata = function(parent_dir, entry, require_stat, cb)
if entry[FIELD_TYPE] == "link" then
local entry_path = fs.posix_to_os_path(parent_dir .. entry[FIELD_NAME])
uv.fs_lstat(entry_path, function(stat_err, stat)
if stat_err then
log.warn("Error lstat link file %s: %s", entry_path, stat_err)
return old_fetch_metadata(parent_dir, entry, require_stat, cb)
end
assert(stat)
entry[FIELD_TYPE] = stat.type
local meta = entry[FIELD_META]
if not meta then
meta = {}
entry[FIELD_META] = meta
end
meta.stat = stat
old_fetch_metadata(parent_dir, entry, require_stat, cb)
end)
else
return old_fetch_metadata(parent_dir, entry, require_stat, cb)
end
end
end
---@param url string
---@param column_defs string[]
---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
local function list_windows_drives(url, column_defs, cb)
local fetch_meta = columns.get_metadata_fetcher(M, column_defs)
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,
@ -314,14 +427,8 @@ local function list_windows_drives(url, column_defs, cb)
else
disk = disk:gsub(":%s*$", "")
local cache_entry = cache.create_entry(url, disk, "directory")
fetch_meta(url, cache_entry, function(err)
if err then
complete_disk_cb(err)
else
table.insert(internal_entries, cache_entry)
complete_disk_cb()
end
end)
table.insert(internal_entries, cache_entry)
fetch_entry_metadata(path, cache_entry, require_stat, complete_disk_cb)
end
end
end,
@ -341,7 +448,7 @@ M.list = function(url, column_defs, cb)
return list_windows_drives(url, column_defs, cb)
end
local dir = fs.posix_to_os_path(path)
local fetch_meta = columns.get_metadata_fetcher(M, column_defs)
local require_stat = columns_require_stat(column_defs)
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(dir, function(open_err, fd)
@ -373,28 +480,8 @@ M.list = function(url, column_defs, cb)
end)
for _, entry in ipairs(entries) do
local cache_entry = cache.create_entry(url, entry.name, entry.type)
fetch_meta(url, cache_entry, function(meta_err)
table.insert(internal_entries, cache_entry)
local meta = cache_entry[FIELD_META]
-- Make sure we always get fs_stat info for links
if entry.type == "link" then
read_link_data(fs.join(dir, entry.name), function(link_err, link, link_stat)
if link_err then
poll(link_err)
else
if not meta then
meta = {}
cache_entry[FIELD_META] = meta
end
meta.link = link
meta.link_stat = link_stat
poll()
end
end)
else
poll()
end
end)
table.insert(internal_entries, cache_entry)
fetch_entry_metadata(path, cache_entry, require_stat, poll)
end
else
uv.fs_closedir(fd, function(close_err)
@ -412,26 +499,6 @@ M.list = function(url, column_defs, cb)
end, 10000)
end
---@type nil|integer[]
local _group_ids
---@return integer[]
local function get_group_ids()
if not _group_ids then
local output = vim.fn.system({ "id", "-G" })
if vim.v.shell_error == 0 then
_group_ids = vim.tbl_map(tonumber, vim.split(output, "%s+", { trimempty = true }))
else
-- If the id command fails, fall back to just using the process group
_group_ids = { uv.getgid() }
vim.notify(
"[oil] missing the `id` command. Some directories may not be modifiable even if you have group access.",
vim.log.levels.WARN
)
end
end
return _group_ids
end
---@param bufnr integer
---@return boolean
M.is_modifiable = function(bufnr)
@ -447,20 +514,8 @@ M.is_modifiable = function(bufnr)
return true
end
-- Can't do permissions checks on windows
if fs.is_windows then
return true
end
local uid = uv.getuid()
local rwx = stat.mode
if uid == stat.uid then
rwx = bit.bor(rwx, bit.rshift(stat.mode, 6))
end
if vim.tbl_contains(get_group_ids(), stat.gid) then
rwx = bit.bor(rwx, bit.rshift(stat.mode, 3))
end
return bit.band(rwx, 2) ~= 0
-- fs_access can return nil, force boolean return
return uv.fs_access(dir, "W") == true
end
---@param action oil.Action
@ -484,7 +539,7 @@ M.render_action = function(action)
return string.format("DELETE %s", short_path)
end
elseif action.type == "move" or action.type == "copy" then
local dest_adapter = 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
local _, src_path = util.parse_url(action.src_url)
assert(src_path)
@ -554,30 +609,22 @@ M.perform_action = function(action, cb)
if config.git.rm(path) then
local old_cb = cb
cb = function(err)
cb = vim.schedule_wrap(function(err)
if not err then
git.rm(path, old_cb)
else
old_cb(err)
end
end
end)
end
if config.delete_to_trash then
if config.trash_command then
vim.notify_once(
"Oil now has native support for trash. Remove the `trash_command` from your config to try it out!",
vim.log.levels.WARN
)
trash.recursive_delete(path, cb)
else
require("oil.adapters.trash").delete_to_trash(path, cb)
end
require("oil.adapters.trash").delete_to_trash(path, cb)
else
fs.recursive_delete(action.entry_type, path, cb)
end
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
local _, src_path = util.parse_url(action.src_url)
assert(src_path)
@ -595,7 +642,7 @@ M.perform_action = function(action, cb)
cb("files adapter doesn't support cross-adapter move")
end
elseif action.type == "copy" then
local dest_adapter = 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
local _, src_path = util.parse_url(action.src_url)
assert(src_path)

View file

@ -1,6 +1,6 @@
local M = {}
---@param exe_modifier nil|false|string
---@param exe_modifier false|string
---@param num integer
---@return string
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

@ -50,6 +50,7 @@ M.parse_url = function(oil_url)
error(string.format("Malformed SSH url: %s", oil_url))
end
---@cast ret oil.sshUrl
return ret
end
@ -125,7 +126,7 @@ ssh_columns.permissions = {
compare = function(entry, parsed_value)
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 old_mode = bit.band(meta.mode, mask)
if parsed_value ~= old_mode then
@ -168,7 +169,7 @@ ssh_columns.size = {
get_sort_value = function(entry)
local meta = entry[FIELD_META]
if meta.size then
if meta and meta.size then
return meta.size
else
return 0
@ -302,8 +303,8 @@ M.perform_action = function(action, cb)
local conn = get_connection(action.url)
conn:rm(res.path, cb)
elseif action.type == "move" then
local src_adapter = config.get_adapter_by_scheme(action.src_url)
local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter == M and dest_adapter == M then
local src_res = M.parse_url(action.src_url)
local dest_res = M.parse_url(action.dest_url)
@ -323,8 +324,8 @@ M.perform_action = function(action, cb)
cb("We should never attempt to move across adapters")
end
elseif action.type == "copy" then
local src_adapter = config.get_adapter_by_scheme(action.src_url)
local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter == M and dest_adapter == M then
local src_res = M.parse_url(action.src_url)
local dest_res = M.parse_url(action.dest_url)
@ -440,6 +441,7 @@ M.goto_file = function()
url.path = vim.fs.dirname(fullpath)
local parurl = url_to_str(url)
---@cast M oil.Adapter
util.adapter_list_all(M, parurl, {}, function(err, entries)
if err then
vim.notify(string.format("Error finding file '%s': %s", fname, err), vim.log.levels.ERROR)

View file

@ -176,6 +176,7 @@ function SSHConnection.new(url)
end
end)
---@cast self oil.sshConnection
return self
end

View file

@ -42,10 +42,17 @@ local function parse_ls_line(line)
local name, size, date, major, minor
if typechar == "c" or typechar == "b" then
major, minor, date, name = rem:match("^(%d+)%s*,%s*(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)")
if name == nil then
major, minor, date, name =
rem:match("^(%d+)%s*,%s*(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)")
end
meta.major = tonumber(major)
meta.minor = tonumber(minor)
else
size, date, name = rem:match("^(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)")
if name == nil then
size, date, name = rem:match("^(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)")
end
meta.size = tonumber(size)
end
meta.iso_modified_date = date
@ -70,6 +77,7 @@ end
---@param url oil.sshUrl
---@return oil.sshFs
function SSHFS.new(url)
---@type oil.sshFs
return setmetatable({
conn = SSHConnection.new(url),
}, {

View file

@ -1,5 +1,5 @@
-- Based on the FreeDesktop.org trash specification
-- https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html
-- https://specifications.freedesktop.org/trash-spec/1.0/
local cache = require("oil.cache")
local config = require("oil.config")
local constants = require("oil.constants")
@ -75,8 +75,13 @@ end
---@param path string
---@return string
local function get_write_trash_dir(path)
local dev = uv.fs_lstat(path).dev
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
@ -126,7 +131,7 @@ end
---@param cb fun(path: string)
M.get_entry_path = function(url, entry, cb)
local internal_entry = assert(cache.get_entry_by_id(entry.id))
local meta = internal_entry[FIELD_META]
local meta = assert(internal_entry[FIELD_META])
---@type oil.TrashInfo
local trash_info = meta.trash_info
if not trash_info then
@ -146,7 +151,7 @@ end
---@field info_file string
---@field original_path string
---@field deletion_date number
---@field stat uv_fs_t
---@field stat uv.aliases.fs_stat_table
---@param info_file string
---@param cb fun(err?: string, info?: oil.TrashInfo)
@ -205,6 +210,7 @@ local function read_trash_info(info_file, cb)
cb(".trashinfo file points to non-existant file")
else
trash_info.stat = trash_stat
---@cast trash_info oil.TrashInfo
cb(nil, trash_info)
end
end)
@ -375,7 +381,7 @@ file_columns.mtime = {
get_sort_value = function(entry)
local meta = entry[FIELD_META]
---@type nil|oil.TrashInfo
local trash_info = meta.trash_info
local trash_info = meta and meta.trash_info
if trash_info then
return trash_info.deletion_date
else
@ -411,7 +417,7 @@ M.filter_action = function(action)
elseif action.type == "delete" then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
return meta.trash_info ~= nil
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))
@ -441,7 +447,7 @@ M.render_action = function(action)
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
---@type oil.TrashInfo
local trash_info = meta.trash_info
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
@ -555,7 +561,7 @@ M.perform_action = function(action, cb)
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
---@type oil.TrashInfo
local trash_info = meta.trash_info
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))
@ -570,7 +576,7 @@ M.perform_action = function(action, cb)
local entry = assert(cache.get_entry_by_url(action.src_url))
local meta = entry[FIELD_META]
---@type oil.TrashInfo
local trash_info = meta.trash_info
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)
@ -590,8 +596,7 @@ M.perform_action = function(action, cb)
if err then
cb(err)
else
---@diagnostic disable-next-line: undefined-field
local stat_type = trash_info.stat.type
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)
@ -602,7 +607,7 @@ M.perform_action = function(action, cb)
local entry = assert(cache.get_entry_by_url(action.src_url))
local meta = entry[FIELD_META]
---@type oil.TrashInfo
local trash_info = meta.trash_info
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")
@ -619,8 +624,7 @@ M.delete_to_trash = function(path, cb)
if err then
cb(err)
else
---@diagnostic disable-next-line: undefined-field
local stat_type = trash_info.stat.type
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)

View file

@ -224,7 +224,6 @@ M.delete_to_trash = function(path, cb)
end
local stat_type = src_stat.type
---@cast stat_type oil.EntryType
fs.recursive_move(stat_type, path, dest, vim.schedule_wrap(cb))
end)
)

View file

@ -37,10 +37,10 @@ local win_addslash = function(path)
end
---@class oil.WindowsTrashInfo
---@field trash_file string?
---@field original_path string?
---@field deletion_date string?
---@field info_file string?
---@field trash_file string
---@field original_path string
---@field deletion_date integer
---@field info_file? string
---@param url string
---@param column_defs string[]
@ -96,6 +96,7 @@ M.list = function(url, column_defs, cb)
end
cache_entry[FIELD_META] = {
stat = nil,
---@type oil.WindowsTrashInfo
trash_info = {
trash_file = entry.Path,
original_path = entry.OriginalPath,
@ -164,8 +165,8 @@ file_columns.mtime = {
get_sort_value = function(entry)
local meta = entry[FIELD_META]
---@type oil.WindowsTrashInfo
local trash_info = meta.trash_info
---@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
@ -199,7 +200,7 @@ M.filter_action = function(action)
elseif action.type == "delete" then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
return meta.trash_info ~= nil
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))
@ -235,7 +236,7 @@ end
M.get_entry_path = function(url, entry, cb)
local internal_entry = assert(cache.get_entry_by_id(entry.id))
local meta = internal_entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
local trash_info = meta.trash_info
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)
@ -265,7 +266,7 @@ M.render_action = function(action)
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
---@type oil.WindowsTrashInfo
local trash_info = meta.trash_info
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
@ -348,7 +349,7 @@ M.perform_action = function(action, cb)
if action.type == "delete" then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
local trash_info = meta.trash_info
local trash_info = meta and meta.trash_info
purge(trash_info, cb)
elseif action.type == "move" then
@ -364,7 +365,7 @@ M.perform_action = function(action, cb)
dest_path = fs.posix_to_os_path(dest_path)
local entry = assert(cache.get_entry_by_url(action.src_url))
local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
local trash_info = meta.trash_info
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)
@ -388,7 +389,7 @@ M.perform_action = function(action, cb)
dest_path = fs.posix_to_os_path(dest_path)
local entry = assert(cache.get_entry_by_url(action.src_url))
local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
local trash_info = meta.trash_info
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")

View file

@ -22,11 +22,22 @@ function PowershellConnection.new(init_command)
self:_init(init_command)
---@type oil.PowershellConnection
return self
end
---@param init_command? string
function PowershellConnection:_init(init_command)
-- For some reason beyond my understanding, at least one of the following
-- things requires `noshellslash` to avoid the embeded powershell process to
-- send only "" to the stdout (never calling the callback because
-- "===DONE(True)===" is never sent to stdout)
-- * vim.fn.jobstart
-- * cmd.exe
-- * powershell.exe
local saved_shellslash = vim.o.shellslash
vim.o.shellslash = false
-- 65001 is the UTF-8 codepage
-- powershell needs to be launched with the UTF-8 codepage to use it for both stdin and stdout
local jid = vim.fn.jobstart({
@ -56,6 +67,7 @@ function PowershellConnection:_init(init_command)
end
end,
})
vim.o.shellslash = saved_shellslash
if jid == 0 then
self:_set_error("passed invalid arguments to 'powershell'")

View file

@ -197,6 +197,7 @@ M.perform_action = function(action)
elseif action.type == "change" then
-- Cache doesn't need to update
else
---@diagnostic disable-next-line: undefined-field
error(string.format("Bad action type: '%s'", action.type))
end
end

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

@ -9,16 +9,16 @@ local FIELD_META = constants.FIELD_META
local all_columns = {}
---@alias oil.ColumnSpec string|table
---@alias oil.ColumnSpec string|{[1]: string, [string]: any}
---@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 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 render_action? fun(action: oil.ChangeAction): string
---@field perform_action? fun(action: oil.ChangeAction, callback: fun(err: nil|string))
---@field get_sort_value? fun(entry: oil.InternalEntry): number|string
---@field create_sort_value_factory? fun(num_entries: integer): fun(entry: oil.InternalEntry): number|string
---@param name string
---@param column oil.ColumnDefinition
@ -53,55 +53,16 @@ M.get_supported_columns = function(adapter_or_scheme)
return ret
end
---@param adapter oil.Adapter
---@param column_defs table[]
---@return fun(parent_url: string, entry: oil.InternalEntry, cb: fun(err: nil|string))
M.get_metadata_fetcher = function(adapter, column_defs)
local keyfetches = {}
local num_keys = 0
for _, def in ipairs(column_defs) do
local name = util.split_config(def)
local column = M.get_column(adapter, name)
if column and column.meta_fields then
for k, v in pairs(column.meta_fields) do
if not keyfetches[k] then
keyfetches[k] = v
num_keys = num_keys + 1
end
end
end
end
if num_keys == 0 then
return function(_, _, cb)
cb()
end
end
return function(parent_url, entry, cb)
cb = util.cb_collect(num_keys, cb)
local meta = {}
entry[FIELD_META] = meta
for k, v in pairs(keyfetches) do
v(parent_url, entry, function(err, value)
if err then
cb(err)
else
meta[k] = value
cb()
end
end)
end
end
end
local EMPTY = { "-", "Comment" }
local EMPTY = { "-", "OilEmpty" }
M.EMPTY = EMPTY
---@param adapter oil.Adapter
---@param col_def oil.ColumnSpec
---@param entry oil.InternalEntry
---@param bufnr integer
---@return oil.TextChunk
M.render_col = function(adapter, col_def, entry)
M.render_col = function(adapter, col_def, entry, bufnr)
local name, conf = util.split_config(col_def)
local column = M.get_column(adapter, name)
if not column then
@ -109,19 +70,7 @@ M.render_col = function(adapter, col_def, entry)
return EMPTY
end
-- Make sure all the required metadata exists before attempting to render
if column.meta_fields then
local meta = entry[FIELD_META]
if not meta then
return EMPTY
end
for k in pairs(column.meta_fields) do
if not meta[k] then
return EMPTY
end
end
end
local chunk = column.render(entry, conf)
local chunk = column.render(entry, conf, bufnr)
if type(chunk) == "table" then
if chunk[1]:match("^%s*$") then
return EMPTY
@ -149,13 +98,13 @@ end
M.parse_col = function(adapter, line, col_def)
local name, conf = util.split_config(col_def)
-- If rendering failed, there will just be a "-"
local empty_col, rem = line:match("^(-%s+)(.*)$")
local empty_col, rem = line:match("^%s*(-%s+)(.*)$")
if empty_col then
return nil, rem
end
local column = M.get_column(adapter, name)
if column then
return column.parse(line, conf)
return column.parse(line:gsub("^%s+", ""), conf)
end
end
@ -251,7 +200,7 @@ local function is_entry_directory(entry)
return true
elseif type == "link" then
local meta = entry[FIELD_META]
return meta and meta.link_stat and meta.link_stat.type == "directory"
return (meta and meta.link_stat and meta.link_stat.type == "directory") == true
else
return false
end
@ -279,8 +228,8 @@ M.register("type", {
end,
})
local function pad_number(int)
return string.format("%012d", int)
local function adjust_number(int)
return string.format("%03d%s", #int, int)
end
M.register("name", {
@ -292,18 +241,33 @@ M.register("name", {
error("Do not use the name column. It is for sorting only")
end,
get_sort_value = function(entry)
local sort_value = entry[FIELD_NAME]
if config.view_options.natural_order then
sort_value = sort_value:gsub("%d+", pad_number)
create_sort_value_factory = function(num_entries)
if
config.view_options.natural_order == false
or (config.view_options.natural_order == "fast" and num_entries > 5000)
then
if config.view_options.case_insensitive then
return function(entry)
return entry[FIELD_NAME]:lower()
end
else
return function(entry)
return entry[FIELD_NAME]
end
end
else
local memo = {}
return function(entry)
if memo[entry] == nil then
local name = entry[FIELD_NAME]:gsub("0*(%d+)", adjust_number)
if config.view_options.case_insensitive then
name = name:lower()
end
memo[entry] = name
end
return memo[entry]
end
end
if config.view_options.case_insensitive then
sort_value = sort_value:lower()
end
return sort_value
end,
})

View file

@ -1,5 +1,3 @@
--stylua: ignore
local default_config = {
-- Oil will take over directory buffers (e.g. `vim .` or `:e src/`)
-- Set to false if you want some other plugin (e.g. netrw) to open when you edit directories.
@ -40,6 +38,8 @@ local default_config = {
-- Note that the cleanup process only starts when none of the oil buffers are currently displayed
cleanup_delay_ms = 2000,
lsp_file_methods = {
-- Enable or disable LSP file operations
enabled = true,
-- Time to wait for LSP file operations to complete before skipping
timeout_ms = 1000,
-- Set to true to autosave buffers that are updated with LSP willRenameFiles
@ -58,22 +58,22 @@ local default_config = {
-- Set to `false` to remove a keymap
-- See :help oil-actions for a list of all available actions
keymaps = {
["g?"] = "actions.show_help",
["g?"] = { "actions.show_help", mode = "n" },
["<CR>"] = "actions.select",
["<C-s>"] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" },
["<C-h>"] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" },
["<C-t>"] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" },
["<C-s>"] = { "actions.select", opts = { vertical = true } },
["<C-h>"] = { "actions.select", opts = { horizontal = true } },
["<C-t>"] = { "actions.select", opts = { tab = true } },
["<C-p>"] = "actions.preview",
["<C-c>"] = "actions.close",
["<C-c>"] = { "actions.close", mode = "n" },
["<C-l>"] = "actions.refresh",
["-"] = "actions.parent",
["_"] = "actions.open_cwd",
["`"] = "actions.cd",
["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" },
["gs"] = "actions.change_sort",
["-"] = { "actions.parent", mode = "n" },
["_"] = { "actions.open_cwd", mode = "n" },
["`"] = { "actions.cd", mode = "n" },
["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
["gs"] = { "actions.change_sort", mode = "n" },
["gx"] = "actions.open_external",
["g."] = "actions.toggle_hidden",
["g\\"] = "actions.toggle_trash",
["g."] = { "actions.toggle_hidden", mode = "n" },
["g\\"] = { "actions.toggle_trash", mode = "n" },
},
-- Set to false to disable all of the above keymaps
use_default_keymaps = true,
@ -82,15 +82,16 @@ local default_config = {
show_hidden = false,
-- This function defines what is considered a "hidden" file
is_hidden_file = function(name, bufnr)
return vim.startswith(name, ".")
local m = name:match("^%.")
return m ~= nil
end,
-- This function defines what will never be shown, even when `show_hidden` is set
is_always_hidden = function(name, bufnr)
return false
end,
-- Sort file names in a more intuitive order for humans. Is less performant,
-- so you may want to set to false if you work with large directories.
natural_order = true,
-- Sort file 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 = {
@ -99,9 +100,15 @@ local default_config = {
{ "type", "asc" },
{ "name", "asc" },
},
-- Customize the highlight group for the file name
highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan)
return nil
end,
},
-- Extra arguments to pass to SCP when moving/copying files over SSH
extra_scp_args = {},
-- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3
extra_s3_args = {},
-- EXPERIMENTAL support for performing file operations with git
git = {
-- Return true to automatically git add/mv/rm files
@ -119,12 +126,15 @@ local default_config = {
float = {
-- Padding around the floating window
padding = 2,
-- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
max_width = 0,
max_height = 0,
border = "rounded",
border = nil,
win_options = {
winblend = 0,
},
-- optionally override the oil buffers window title with custom function: fun(winid: integer): string
get_win_title = nil,
-- preview_split: Split direction: "auto", "left", "right", "above", "below".
preview_split = "auto",
-- This is the config that will be passed to nvim_open_win.
@ -133,8 +143,21 @@ local default_config = {
return conf
end,
},
-- Configuration for the actions floating preview window
preview = {
-- Configuration for the file preview window
preview_win = {
-- Whether the preview window is automatically updated when the cursor is moved
update_on_cursor_moved = true,
-- How to open the preview window "load"|"scratch"|"fast_scratch"
preview_method = "fast_scratch",
-- A function that returns true to disable preview on a file e.g. to avoid lag
disable_preview = function(filename)
return false
end,
-- Window-local options to use for preview window buffers
win_options = {},
},
-- Configuration for the floating action confirmation window
confirmation = {
-- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
-- min_width and max_width can be a single value or a list of mixed integer/float types.
-- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total"
@ -151,12 +174,10 @@ local default_config = {
min_height = { 5, 0.1 },
-- optionally define an integer/float for the exact height of the preview window
height = nil,
border = "rounded",
border = nil,
win_options = {
winblend = 0,
},
-- Whether the preview window is automatically updated when the cursor is moved
update_on_cursor_moved = true,
},
-- Configuration for the floating progress window
progress = {
@ -166,7 +187,7 @@ local default_config = {
max_height = { 10, 0.9 },
min_height = { 5, 0.1 },
height = nil,
border = "rounded",
border = nil,
minimized_border = "none",
win_options = {
winblend = 0,
@ -174,30 +195,242 @@ local default_config = {
},
-- Configuration for the floating SSH window
ssh = {
border = "rounded",
border = nil,
},
-- Configuration for the floating keymaps help window
keymaps_help = {
border = "rounded",
border = nil,
},
}
-- The adapter API hasn't really stabilized yet. We're not ready to advertise or encourage people to
-- write their own adapters, and so there's no real reason to edit these config options. For that
-- reason, I'm taking them out of the section above so they won't show up in the autogen docs.
-- not "oil-s3://" on older neovim versions, since it doesn't open buffers correctly with a number
-- in the name
local oil_s3_string = vim.fn.has("nvim-0.12") == 1 and "oil-s3://" or "oil-sss://"
default_config.adapters = {
["oil://"] = "files",
["oil-ssh://"] = "ssh",
[oil_s3_string] = "s3",
["oil-trash://"] = "trash",
}
default_config.adapter_aliases = {}
-- We want the function in the default config for documentation generation, but if we nil it out
-- here we can get some performance wins
default_config.view_options.highlight_filename = nil
---@class oil.Config
---@field adapters table<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 = {}
-- 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)
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
new_conf.keymaps = opts.keymaps or {}
elseif opts.keymaps then
-- We don't want to deep merge the keymaps, we want any keymap defined by the user to override
-- everything about the default.
for k, v in pairs(opts.keymaps) do
new_conf.keymaps[k] = v
end
end
-- Backwards compatibility for old versions that don't support winborder
if vim.fn.has("nvim-0.11") == 0 then
new_conf = vim.tbl_deep_extend("keep", new_conf, {
float = { border = "rounded" },
confirmation = { border = "rounded" },
progress = { border = "rounded" },
ssh = { border = "rounded" },
keymaps_help = { border = "rounded" },
})
end
-- Backwards compatibility. We renamed the 'preview' window config to be called 'confirmation'.
if opts.preview and not opts.confirmation then
new_conf.confirmation = vim.tbl_deep_extend("keep", opts.preview, default_config.confirmation)
end
-- Backwards compatibility. We renamed the 'preview' config to 'preview_win'
if opts.preview and opts.preview.update_on_cursor_moved ~= nil then
new_conf.preview_win.update_on_cursor_moved = opts.preview.update_on_cursor_moved
end
if new_conf.lsp_rename_autosave ~= nil then
@ -243,10 +476,6 @@ M.get_adapter_by_scheme = function(scheme)
if adapter == nil then
local name = M.adapters[scheme]
if not name then
vim.notify(
string.format("Could not find oil adapter for scheme '%s'", scheme),
vim.log.levels.ERROR
)
return nil
end
local ok
@ -257,7 +486,6 @@ M.get_adapter_by_scheme = function(scheme)
else
M._adapter_by_scheme[scheme] = false
adapter = false
vim.notify(string.format("Could not find oil adapter '%s'", name), vim.log.levels.ERROR)
end
end
if adapter then

View file

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

View file

@ -1,3 +1,4 @@
local log = require("oil.log")
local M = {}
local uv = vim.uv or vim.loop
@ -217,7 +218,7 @@ M.recursive_delete = function(entry_type, path, cb)
local waiting = #entries
local complete
complete = function(err2)
if err then
if err2 then
complete = function() end
return inner_cb(err2)
end
@ -245,6 +246,37 @@ M.recursive_delete = function(entry_type, path, cb)
end, 10000)
end
---Move the undofile for the file at src_path to dest_path
---@param src_path string
---@param dest_path string
---@param copy boolean
local move_undofile = vim.schedule_wrap(function(src_path, dest_path, copy)
local undofile = vim.fn.undofile(src_path)
uv.fs_stat(
undofile,
vim.schedule_wrap(function(stat_err)
if stat_err then
-- undofile doesn't exist
return
end
local dest_undofile = vim.fn.undofile(dest_path)
if copy then
uv.fs_copyfile(src_path, dest_path, function(err)
if err then
log.warn("Error copying undofile %s: %s", undofile, err)
end
end)
else
uv.fs_rename(undofile, dest_undofile, function(err)
if err then
log.warn("Error moving undofile %s: %s", undofile, err)
end
end)
end
end)
)
end)
---@param entry_type oil.EntryType
---@param src_path string
---@param dest_path string
@ -262,6 +294,7 @@ M.recursive_copy = function(entry_type, src_path, dest_path, cb)
end
if entry_type ~= "directory" then
uv.fs_copyfile(src_path, dest_path, { excl = true }, cb)
move_undofile(src_path, dest_path, true)
return
end
uv.fs_stat(src_path, function(stat_err, src_stat)
@ -287,7 +320,7 @@ M.recursive_copy = function(entry_type, src_path, dest_path, cb)
local waiting = #entries
local complete
complete = function(err2)
if err then
if err2 then
complete = function() end
return inner_cb(err2)
end
@ -333,6 +366,9 @@ M.recursive_move = function(entry_type, src_path, dest_path, cb)
end
end)
else
if entry_type ~= "directory" then
move_undofile(src_path, dest_path, false)
end
cb()
end
end)

View file

@ -7,7 +7,7 @@ local M = {}
---@field parsed_name nil|string
---@field meta nil|table
---@alias oil.EntryType "file"|"directory"|"socket"|"link"|"fifo"
---@alias oil.EntryType uv.aliases.fs_types
---@alias oil.HlRange { [1]: string, [2]: integer, [3]: integer } A tuple of highlight group name, col_start, col_end
---@alias oil.HlTuple { [1]: string, [2]: string } A tuple of text, highlight group
---@alias oil.HlRangeTuple { [1]: string, [2]: oil.HlRange[] } A tuple of text, internal highlights
@ -129,7 +129,7 @@ M.set_sort = function(sort)
end
---Change how oil determines if the file is hidden
---@param is_hidden_file fun(filename: string, bufnr: nil|integer): boolean Return true if the file/dir should be hidden
---@param is_hidden_file fun(filename: string, bufnr: integer): boolean Return true if the file/dir should be hidden
M.set_is_hidden_file = function(is_hidden_file)
require("oil.view").set_is_hidden_file(is_hidden_file)
end
@ -222,7 +222,7 @@ M.get_buffer_parent_url = function(bufname, use_oil_parent)
if not use_oil_parent then
return bufname
end
local adapter = config.get_adapter_by_scheme(scheme)
local adapter = assert(config.get_adapter_by_scheme(scheme))
local parent_url
if adapter and adapter.get_parent then
local adapter_scheme = config.adapter_to_scheme[adapter.name]
@ -239,18 +239,21 @@ M.get_buffer_parent_url = function(bufname, use_oil_parent)
end
end
---@class (exact) oil.OpenOpts
---@field preview? oil.OpenPreviewOpts When present, open the preview window after opening oil
---Open oil browser in a floating window
---@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
M.open_float = function(dir)
---@param dir? string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
---@param opts? oil.OpenOpts
---@param cb? fun() Called after the oil buffer is ready
M.open_float = function(dir, opts, cb)
opts = opts or {}
local config = require("oil.config")
local layout = require("oil.layout")
local util = require("oil.util")
local view = require("oil.view")
local parent_url, basename = M.get_url_for_path(dir)
if not parent_url then
return
end
if basename then
view.set_last_cursor(parent_url, basename)
end
@ -288,19 +291,6 @@ M.open_float = function(dir)
})
)
---Recalculate the window title for the current buffer
local function get_title()
local src_buf = vim.api.nvim_win_get_buf(winid)
local title = vim.api.nvim_buf_get_name(src_buf)
local scheme, path = util.parse_url(title)
if config.adapters[scheme] == "files" then
assert(path)
local fs = require("oil.fs")
title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~")
end
return title
end
table.insert(
autocmds,
vim.api.nvim_create_autocmd("BufWinEnter", {
@ -324,7 +314,7 @@ M.open_float = function(dir)
col = cur_win_opts.col,
width = cur_win_opts.width,
height = cur_win_opts.height,
title = get_title(),
title = util.get_title(winid),
})
end
end,
@ -337,6 +327,14 @@ M.open_float = function(dir)
vim.api.nvim_set_option_value("buflisted", config.buf_options.buflisted, { buf = 0 })
end
util.run_after_load(0, function()
if opts.preview then
M.open_preview(opts.preview, cb)
elseif cb then
cb()
end
end)
if vim.fn.has("nvim-0.9") == 0 then
util.add_title_to_win(winid)
end
@ -344,11 +342,16 @@ end
---Open oil browser in a floating window, or close it if open
---@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
M.toggle_float = function(dir)
---@param opts? oil.OpenOpts
---@param cb? fun() Called after the oil buffer is ready
M.toggle_float = function(dir, opts, cb)
if vim.w.is_oil_win then
M.close()
if cb then
cb()
end
else
M.open_float(dir)
M.open_float(dir, opts, cb)
end
end
@ -370,15 +373,15 @@ local function update_preview_window(oil_bufnr)
end
---Open oil browser for a directory
---@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
M.open = function(dir)
---@param dir? string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
---@param opts? oil.OpenOpts
---@param cb? fun() Called after the oil buffer is ready
M.open = function(dir, opts, cb)
opts = opts or {}
local config = require("oil.config")
local util = require("oil.util")
local view = require("oil.view")
local parent_url, basename = M.get_url_for_path(dir)
if not parent_url then
return
end
if basename then
view.set_last_cursor(parent_url, basename)
end
@ -388,12 +391,25 @@ M.open = function(dir)
vim.api.nvim_set_option_value("buflisted", config.buf_options.buflisted, { buf = 0 })
end
util.run_after_load(0, function()
if opts.preview then
M.open_preview(opts.preview, cb)
elseif cb then
cb()
end
end)
-- If preview window exists, update its content
update_preview_window()
end
---@class oil.CloseOpts
---@field exit_if_last_buf? boolean Exit vim if this oil buffer is the last open buffer
---Restore the buffer that was present when oil was opened
M.close = function()
---@param opts? oil.CloseOpts
M.close = function(opts)
opts = opts or {}
-- If we're in a floating oil window, close it and try to restore focus to the original window
if vim.w.is_oil_win then
local original_winid = vim.w.oil_original_win
@ -416,18 +432,26 @@ M.close = function()
-- buffer first
local oilbuf = vim.api.nvim_get_current_buf()
ok = pcall(vim.cmd.bprev)
-- If `bprev` failed, there are no buffers open
if not ok then
-- If `bprev` failed, there are no buffers open so we should create a new one with enew
vim.cmd.enew()
-- either exit or create a new blank buffer
if opts.exit_if_last_buf then
vim.cmd.quit()
else
vim.cmd.enew()
end
end
vim.api.nvim_buf_delete(oilbuf, { force = true })
end
---@class oil.OpenPreviewOpts
---@field vertical? boolean Open the buffer in a vertical split
---@field horizontal? boolean Open the buffer in a horizontal split
---@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier
---Preview the entry under the cursor in a split
---@param opts nil|table
--- vertical boolean Open the buffer in a vertical split
--- horizontal boolean Open the buffer in a horizontal split
--- split "aboveleft"|"belowright"|"topleft"|"botright" Split modifier
---@param opts? oil.OpenPreviewOpts
---@param callback? fun(err: nil|string) Called once the preview window has been opened
M.open_preview = function(opts, callback)
opts = opts or {}
local config = require("oil.config")
@ -454,7 +478,7 @@ M.open_preview = function(opts, callback)
end
end
local preview_win = util.get_preview_win()
local preview_win = util.get_preview_win({ include_not_owned = true })
local prev_win = vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_get_current_buf()
@ -501,6 +525,7 @@ M.open_preview = function(opts, callback)
preview_win = vim.api.nvim_open_win(bufnr, true, win_opts)
vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = preview_win })
vim.api.nvim_win_set_var(preview_win, "oil_preview", true)
vim.api.nvim_set_current_win(prev_win)
elseif vim.fn.has("nvim-0.9") == 1 then
vim.api.nvim_win_set_config(preview_win, { title = entry_title })
@ -523,6 +548,8 @@ M.open_preview = function(opts, callback)
end
util.get_edit_path(bufnr, entry, function(normalized_url)
local mc = package.loaded["multicursor-nvim"]
local has_multicursors = mc and mc.hasCursors()
local is_visual_mode = util.is_visual_mode()
if preview_win then
if is_visual_mode then
@ -532,14 +559,29 @@ M.open_preview = function(opts, callback)
end
end
local filebufnr = vim.fn.bufadd(normalized_url)
local entry_is_file = not vim.endswith(normalized_url, "/")
local filebufnr
if entry_is_file then
if config.preview_win.disable_preview(normalized_url) then
filebufnr = vim.api.nvim_create_buf(false, true)
vim.bo[filebufnr].bufhidden = "wipe"
vim.bo[filebufnr].buftype = "nofile"
util.render_text(filebufnr, "Preview disabled", { winid = preview_win })
elseif
config.preview_win.preview_method ~= "load"
and not util.file_matches_bufreadcmd(normalized_url)
then
filebufnr =
util.read_file_to_scratch_buffer(normalized_url, config.preview_win.preview_method)
end
end
-- If we're previewing a file that hasn't been opened yet, make sure it gets deleted after
-- we close the window
if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then
vim.bo[filebufnr].bufhidden = "wipe"
vim.b[filebufnr].oil_preview_buffer = true
if not filebufnr then
filebufnr = vim.fn.bufadd(normalized_url)
if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then
vim.bo[filebufnr].bufhidden = "wipe"
vim.b[filebufnr].oil_preview_buffer = true
end
end
---@diagnostic disable-next-line: param-type-mismatch
@ -553,10 +595,23 @@ M.open_preview = function(opts, callback)
vim.api.nvim_echo({ { err, "Error" } }, true, {})
end
-- If we called open_preview during an autocmd, then the edit command may not trigger the
-- BufReadCmd to load the buffer. So we need to do it manually.
if util.is_oil_bufnr(filebufnr) then
M.load_oil_buffer(filebufnr)
end
vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 })
vim.api.nvim_win_set_var(0, "oil_preview", true)
for k, v in pairs(config.preview_win.win_options) do
vim.api.nvim_set_option_value(k, v, { scope = "local", win = preview_win })
end
vim.w.oil_entry_id = entry.id
vim.w.oil_source_win = prev_win
if is_visual_mode then
if has_multicursors then
hack_set_win(prev_win)
mc.restoreCursors()
elseif is_visual_mode then
hack_set_win(prev_win)
-- Restore the visual selection
vim.cmd.normal({ args = { "gv" }, bang = true })
@ -573,6 +628,7 @@ end
---@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier
---@field tab? boolean Open the buffer in a new tab
---@field close? boolean Close the original oil buffer once selection is made
---@field handle_buffer_callback? fun(buf_id: integer) If defined, all other buffer related options here would be ignored. This callback allows you to take over the process of opening the buffer yourself.
---Select the entry under the cursor
---@param opts nil|oil.SelectOpts
@ -585,14 +641,6 @@ M.select = function(opts, callback)
local FIELD_META = constants.FIELD_META
opts = vim.tbl_extend("keep", opts or {}, {})
if opts.preview then
vim.notify_once(
"Deprecated: do not call oil.select with preview=true. Use oil.open_preview instead.\nThis shim will be removed on 2025-01-01"
)
M.open_preview(opts, callback)
return
end
local function finish(err)
if err then
vim.notify(err, vim.log.levels.ERROR)
@ -690,7 +738,7 @@ M.select = function(opts, callback)
else
-- Close floating window before opening a file
if vim.w.is_oil_win then
vim.api.nvim_win_close(0, false)
M.close()
end
end
@ -701,7 +749,7 @@ M.select = function(opts, callback)
vertical = opts.vertical,
horizontal = opts.horizontal,
split = opts.split,
keepalt = true,
keepalt = false,
}
local filebufnr = vim.fn.bufadd(normalized_url)
local entry_is_file = not vim.endswith(normalized_url, "/")
@ -715,18 +763,24 @@ M.select = function(opts, callback)
local cmd = "buffer"
if opts.tab then
vim.cmd.tabnew({ mods = mods })
-- Make sure the new buffer from tabnew gets cleaned up
vim.bo.bufhidden = "wipe"
elseif opts.split then
cmd = "sbuffer"
end
---@diagnostic disable-next-line: param-type-mismatch
local ok, err = pcall(vim.cmd, {
cmd = cmd,
args = { filebufnr },
mods = mods,
})
-- Ignore swapfile errors
if not ok and err and not err:match("^Vim:E325:") then
vim.api.nvim_echo({ { err, "Error" } }, true, {})
if opts.handle_buffer_callback ~= nil then
opts.handle_buffer_callback(filebufnr)
else
---@diagnostic disable-next-line: param-type-mismatch
local ok, err = pcall(vim.cmd, {
cmd = cmd,
args = { filebufnr },
mods = mods,
})
-- Ignore swapfile errors
if not ok and err and not err:match("^Vim:E325:") then
vim.api.nvim_echo({ { err, "Error" } }, true, {})
end
end
open_next_entry(cb)
@ -779,11 +833,26 @@ end
---@private
M._get_highlights = function()
return {
{
name = "OilEmpty",
link = "Comment",
desc = "Empty column values",
},
{
name = "OilHidden",
link = "Comment",
desc = "Hidden entry in an oil buffer",
},
{
name = "OilDir",
link = "Directory",
desc = "Directory names in an oil buffer",
},
{
name = "OilDirHidden",
link = "OilHidden",
desc = "Hidden directory names in an oil buffer",
},
{
name = "OilDirIcon",
link = "OilDir",
@ -794,21 +863,61 @@ M._get_highlights = function()
link = "Keyword",
desc = "Socket files in an oil buffer",
},
{
name = "OilSocketHidden",
link = "OilHidden",
desc = "Hidden socket files in an oil buffer",
},
{
name = "OilLink",
link = nil,
desc = "Soft links in an oil buffer",
},
{
name = "OilOrphanLink",
link = nil,
desc = "Orphaned soft links in an oil buffer",
},
{
name = "OilLinkHidden",
link = "OilHidden",
desc = "Hidden soft links in an oil buffer",
},
{
name = "OilOrphanLinkHidden",
link = "OilLinkHidden",
desc = "Hidden orphaned soft links in an oil buffer",
},
{
name = "OilLinkTarget",
link = "Comment",
desc = "The target of a soft link",
},
{
name = "OilOrphanLinkTarget",
link = "DiagnosticError",
desc = "The target of an orphaned soft link",
},
{
name = "OilLinkTargetHidden",
link = "OilHidden",
desc = "The target of a hidden soft link",
},
{
name = "OilOrphanLinkTargetHidden",
link = "OilOrphanLinkTarget",
desc = "The target of an hidden orphaned soft link",
},
{
name = "OilFile",
link = nil,
desc = "Normal files in an oil buffer",
},
{
name = "OilFileHidden",
link = "OilHidden",
desc = "Hidden normal files in an oil buffer",
},
{
name = "OilCreate",
link = "DiagnosticInfo",
@ -924,8 +1033,9 @@ local function restore_alt_buf()
end
end
---@private
---@param bufnr integer
local function load_oil_buffer(bufnr)
M.load_oil_buffer = function(bufnr)
local config = require("oil.config")
local keymap_util = require("oil.keymap_util")
local loading = require("oil.loading")
@ -939,6 +1049,11 @@ local function load_oil_buffer(bufnr)
util.rename_buffer(bufnr, bufname)
end
-- Early return if we're already loading or have already loaded this buffer
if loading.is_loading(bufnr) or vim.b[bufnr].filetype ~= nil then
return
end
local adapter = assert(config.get_adapter_by_scheme(scheme))
if vim.endswith(bufname, "/") then
@ -1023,13 +1138,14 @@ M.setup = function(opts)
config.setup(opts)
set_colors()
vim.api.nvim_create_user_command("Oil", function(args)
local callback = function(args)
local util = require("oil.util")
if args.smods.tab == 1 then
if args.smods.tab > 0 then
vim.cmd.tabnew()
end
local float = false
local trash = false
local preview = false
local i = 1
while i <= #args.fargs do
local v = args.fargs[i]
@ -1039,6 +1155,11 @@ M.setup = function(opts)
elseif v == "--trash" then
trash = true
table.remove(args.fargs, i)
elseif v == "--preview" then
-- In the future we may want to support specifying options for the preview window (e.g.
-- vertical/horizontal), but if you want that level of control maybe just use the API
preview = true
table.remove(args.fargs, i)
elseif v == "--progress" then
local mutator = require("oil.mutator")
if mutator.is_mutating() then
@ -1052,23 +1173,34 @@ M.setup = function(opts)
end
end
if not float and (args.smods.vertical or args.smods.split ~= "") then
if not float and (args.smods.vertical or args.smods.horizontal or args.smods.split ~= "") then
local range = args.count > 0 and { args.count } or nil
local cmdargs = { mods = { split = args.smods.split }, range = range }
if args.smods.vertical then
vim.cmd.vsplit({ mods = { split = args.smods.split } })
vim.cmd.vsplit(cmdargs)
else
vim.cmd.split({ mods = { split = args.smods.split } })
vim.cmd.split(cmdargs)
end
end
local method = float and "open_float" or "open"
local path = args.fargs[1]
local open_opts = {}
if trash then
local url = M.get_url_for_path(path, false)
local _, new_path = util.parse_url(url)
path = "oil-trash://" .. new_path
end
M[method](path)
end, { desc = "Open oil file browser on a directory", nargs = "*", complete = "dir" })
if preview then
open_opts.preview = {}
end
M[method](path, open_opts)
end
vim.api.nvim_create_user_command(
"Oil",
callback,
{ desc = "Open oil file browser on a directory", nargs = "*", complete = "dir", count = true }
)
local aug = vim.api.nvim_create_augroup("Oil", {})
if config.default_file_explorer then
@ -1114,7 +1246,7 @@ M.setup = function(opts)
pattern = scheme_pattern,
nested = true,
callback = function(params)
load_oil_buffer(params.buf)
M.load_oil_buffer(params.buf)
end,
})
vim.api.nvim_create_autocmd("BufWriteCmd", {
@ -1149,8 +1281,7 @@ M.setup = function(opts)
end)
vim.cmd.doautocmd({ args = { "BufWritePost", params.file }, mods = { silent = true } })
else
local adapter = config.get_adapter_by_scheme(bufname)
assert(adapter)
local adapter = assert(config.get_adapter_by_scheme(bufname))
adapter.write_file(params.buf)
end
end,
@ -1177,7 +1308,10 @@ M.setup = function(opts)
local util = require("oil.util")
local bufname = vim.api.nvim_buf_get_name(0)
local scheme = util.parse_url(bufname)
if scheme and config.adapters[scheme] then
local is_oil_buf = scheme and config.adapters[scheme]
-- We want to filter out oil buffers that are not directories (i.e. ssh files)
local is_oil_dir_or_unknown = (vim.bo.filetype == "oil" or vim.bo.filetype == "")
if is_oil_buf and is_oil_dir_or_unknown then
local view = require("oil.view")
view.maybe_set_cursor()
-- While we are in an oil buffer, set the alternate file to the buffer we were in prior to
@ -1285,7 +1419,7 @@ M.setup = function(opts)
local util = require("oil.util")
local scheme = util.parse_url(params.file)
if config.adapters[scheme] and vim.api.nvim_buf_line_count(params.buf) == 1 then
load_oil_buffer(params.buf)
M.load_oil_buffer(params.buf)
end
end,
})
@ -1294,7 +1428,7 @@ M.setup = function(opts)
if maybe_hijack_directory_buffer(bufnr) and vim.v.vim_did_enter == 1 then
-- manually call load on a hijacked directory buffer if vim has already entered
-- (the BufReadCmd will not trigger)
load_oil_buffer(bufnr)
M.load_oil_buffer(bufnr)
end
end

View file

@ -19,7 +19,18 @@ local function resolve(rhs)
elseif type(rhs) == "table" then
local opts = vim.deepcopy(rhs)
-- We support passing in a `callback` key, or using the 1 index as the rhs of the keymap
local callback = resolve(opts.callback or opts[1])
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
if type(rhs.callback) == "string" then
local action_opts, action_mode
@ -78,31 +89,30 @@ M.show_help = function(keymaps)
end
end
local col_left = {}
local col_desc = {}
local max_lhs = 1
local keymap_entries = {}
for k, rhs in pairs(keymaps) do
local all_lhs = lhs_to_all_lhs[k]
if all_lhs then
local _, opts = resolve(rhs)
local keystr = table.concat(all_lhs, "/")
max_lhs = math.max(max_lhs, vim.api.nvim_strwidth(keystr))
table.insert(col_left, { str = keystr, all_lhs = all_lhs })
table.insert(col_desc, opts.desc or "")
table.insert(keymap_entries, { str = keystr, all_lhs = all_lhs, desc = opts.desc or "" })
end
end
table.sort(keymap_entries, function(a, b)
return a.desc < b.desc
end)
local lines = {}
local highlights = {}
local max_line = 1
for i = 1, #col_left do
local left = col_left[i]
local desc = col_desc[i]
local line = string.format(" %s %s", util.rpad(left.str, max_lhs), desc)
for _, entry in ipairs(keymap_entries) do
local line = string.format(" %s %s", util.pad_align(entry.str, max_lhs, "left"), entry.desc)
max_line = math.max(max_line, vim.api.nvim_strwidth(line))
table.insert(lines, line)
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)
table.insert(highlights, { "Special", #lines, start, start + keywidth })
start = start + keywidth + 1

View file

@ -1,10 +1,15 @@
local M = {}
---@param value number
---@return boolean
local function is_float(value)
local _, p = math.modf(value)
return p ~= 0
end
---@param value number
---@param max_value number
---@return number
local function calc_float(value, max_value)
if value and is_float(value) then
return math.min(max_value, value * max_value)
@ -93,7 +98,7 @@ M.calculate_height = function(desired_height, opts)
)
end
---@class (exact) conform.WinLayout
---@class (exact) oil.WinLayout
---@field width integer
---@field height integer
---@field row integer
@ -110,11 +115,13 @@ M.get_fullscreen_win_opts = function()
width = width - 2 -- The border consumes 1 col on each side
end
if config.float.max_width > 0 then
width = math.min(width, config.float.max_width)
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
height = math.min(height, config.float.max_height)
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
@ -134,14 +141,15 @@ end
---@param winid integer
---@param direction "above"|"below"|"left"|"right"|"auto"
---@param gap integer
---@return conform.WinLayout root_dim New dimensions of the original window
---@return conform.WinLayout new_dim New dimensions of the new window
---@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,
@ -176,6 +184,11 @@ M.split_window = function(winid, direction, gap)
return dim_root, dim_new
end
---@param desired_width integer
---@param desired_height integer
---@param opts table
---@return integer width
---@return integer height
M.calculate_dims = function(desired_width, desired_height, opts)
local width = M.calculate_width(desired_width, opts)
local height = M.calculate_height(desired_height, opts)

View file

@ -74,7 +74,8 @@ M.set_loading = function(bufnr, is_loading)
M.set_loading(bufnr, false)
return
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)
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

View file

@ -68,24 +68,34 @@ local function get_matching_paths(client, filters, paths)
end
-- Some language servers use forward slashes as path separators on Windows (LuaLS)
if fs.is_windows then
-- 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
-- HACK around https://github.com/neovim/neovim/issues/28931
-- find alternations and sort them by length to try to match the longest first
if vim.fn.has("nvim-0.11") == 0 then
glob = glob:gsub("{(.*)}", function(s)
local pieces = vim.split(s, ",")
table.sort(pieces, function(a, b)
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)
return "{" .. table.concat(pieces, ",") .. "}"
end)
end
end
return "{" .. table.concat(filtered, ",") .. "}"
end)
glob_to_match = vim.glob.to_lpeg(glob)
end
@ -167,8 +177,13 @@ local function will_file_operation(method, capability_name, files, options)
}
end, matching_files),
}
---@diagnostic disable-next-line: invisible
local result, err = client.request_sync(method, params, options.timeout_ms or 1000, 0)
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)
@ -204,8 +219,12 @@ local function did_file_operation(method, capability_name, files)
}
end, matching_files),
}
---@diagnostic disable-next-line: invisible
client.notify(method, params)
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
@ -280,9 +299,15 @@ function M.will_rename_files(files, options)
}
end, matching_files),
}
local result, err =
---@diagnostic disable-next-line: invisible
client.request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0)
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)
@ -313,8 +338,12 @@ function M.did_rename_files(files)
}
end, matching_files),
}
---@diagnostic disable-next-line: invisible
client.notify(ms.workspace_didRenameFiles, params)
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

View file

@ -82,6 +82,8 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
else
line = adapter.render_action(action)
end
-- We can't handle lines with newlines in them
line = line:gsub("\n", "")
table.insert(lines, line)
local line_width = vim.api.nvim_strwidth(line)
if line_width > max_line_width then
@ -91,7 +93,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
table.insert(lines, "")
-- Create the floating window
local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.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, {
relative = "editor",
width = width,
@ -100,7 +102,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
col = math.floor((layout.get_editor_width() - width) / 2),
zindex = 152, -- render on top of the floating window title
style = "minimal",
border = config.preview.border,
border = config.confirmation.border,
})
if not ok then
vim.notify(string.format("Error showing oil preview window: %s", winid), vim.log.levels.ERROR)
@ -108,7 +110,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
end
vim.bo[bufnr].filetype = "oil_preview"
vim.bo[bufnr].syntax = "oil_preview"
for k, v in pairs(config.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 })
end
@ -155,7 +157,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
vim.api.nvim_create_autocmd("VimResized", {
callback = function()
if vim.api.nvim_win_is_valid(winid) then
width, height = layout.calculate_dims(max_line_width, #lines, config.preview)
width, height = layout.calculate_dims(max_line_width, #lines, config.confirmation)
vim.api.nvim_win_set_config(winid, {
relative = "editor",
width = width,

View file

@ -3,12 +3,12 @@ local Trie = require("oil.mutator.trie")
local cache = require("oil.cache")
local columns = require("oil.columns")
local config = require("oil.config")
local confirmation = require("oil.mutator.confirmation")
local constants = require("oil.constants")
local fs = require("oil.fs")
local lsp_helpers = require("oil.lsp.helpers")
local oil = require("oil")
local parser = require("oil.mutator.parser")
local preview = require("oil.mutator.preview")
local util = require("oil.util")
local view = require("oil.view")
local M = {}
@ -85,7 +85,7 @@ M.create_actions_from_diffs = function(all_diffs)
end
end
for bufnr, diffs in pairs(all_diffs) do
local adapter = util.get_adapter(bufnr)
local adapter = util.get_adapter(bufnr, true)
if not adapter then
error("Missing adapter")
end
@ -140,6 +140,7 @@ M.create_actions_from_diffs = function(all_diffs)
else
local by_id = diff_by_id[diff.id]
-- HACK: set has_delete field on a list-like table of diffs
---@diagnostic disable-next-line: inject-field
by_id.has_delete = true
-- Don't insert the delete. We already know that there is a delete because of the presence
-- in the diff_by_id map. The list will only include the 'new' diffs.
@ -252,11 +253,10 @@ M.enforce_action_order = function(actions)
-- Process children before moving
-- e.g. NEW /a/b BEFORE MOVE /a -> /b
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
src_trie:accum_children_of(action.src_url, ret, function(a)
return a.type == "copy"
end)
-- e.g. CHANGE /a/b BEFORE MOVE /a -> /d
src_trie:accum_children_of(action.src_url, ret)
-- Process remove path before moving to new path
-- e.g. MOVE /a -> /b BEFORE MOVE /c -> /a
src_trie:accum_actions_at(action.dest_url, ret, function(a)
@ -390,7 +390,11 @@ M.process_actions = function(actions, cb)
"User",
{ pattern = "OilActionsPre", modeline = false, data = { actions = actions } }
)
local did_complete = lsp_helpers.will_perform_file_operations(actions)
local did_complete = nil
if config.lsp_file_methods.enabled then
did_complete = lsp_helpers.will_perform_file_operations(actions)
end
-- Convert some cross-adapter moves to a copy + delete
for _, action in ipairs(actions) do
@ -398,6 +402,7 @@ M.process_actions = function(actions, cb)
local _, cross_action = util.get_adapter_for_action(action)
-- Only do the conversion if the cross-adapter support is "copy"
if cross_action == "copy" then
---@diagnostic disable-next-line: assign-type-mismatch
action.type = "copy"
table.insert(actions, {
type = "delete",
@ -443,7 +448,9 @@ M.process_actions = function(actions, cb)
return
end
if idx > #actions then
did_complete()
if did_complete then
did_complete()
end
finish()
return
end
@ -512,7 +519,7 @@ M.try_write_changes = function(confirm, cb)
if vim.bo[bufnr].modified then
local diffs, errors = parser.parse(bufnr)
all_diffs[bufnr] = diffs
local adapter = assert(util.get_adapter(bufnr))
local adapter = assert(util.get_adapter(bufnr, true))
if adapter.filter_error then
errors = vim.tbl_filter(adapter.filter_error, errors)
end
@ -546,10 +553,15 @@ M.try_write_changes = function(confirm, cb)
{ all_errors[curbuf][1].lnum + 1, all_errors[curbuf][1].col }
)
else
---@diagnostic disable-next-line: param-type-mismatch
local bufnr, errs = next(pairs(all_errors))
vim.api.nvim_win_set_buf(0, bufnr)
pcall(vim.api.nvim_win_set_cursor, 0, { errs[1].lnum + 1, errs[1].col })
local bufnr, errs = next(all_errors)
assert(bufnr)
assert(errs)
-- HACK: This is a workaround for the fact that we can't switch buffers in the middle of a
-- BufWriteCmd.
vim.schedule(function()
vim.api.nvim_win_set_buf(0, bufnr)
pcall(vim.api.nvim_win_set_cursor, 0, { errs[1].lnum + 1, errs[1].col })
end)
end
unlock()
cb("Error parsing oil buffers")
@ -557,7 +569,7 @@ M.try_write_changes = function(confirm, cb)
end
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
unlock()
cb("Canceled")

View file

@ -95,7 +95,7 @@ M.parse_line = function(adapter, line, column_defs)
local name = util.split_config(def)
local range = { start }
local start_len = string.len(rem)
value, rem = columns.parse_col(adapter, rem, def)
value, rem = columns.parse_col(adapter, assert(rem), def)
if not rem then
return nil, string.format("Parsing %s failed", name)
end
@ -156,7 +156,7 @@ M.parse = function(bufnr)
---@type oil.ParseError[]
local errors = {}
local bufname = vim.api.nvim_buf_get_name(bufnr)
local adapter = util.get_adapter(bufnr)
local adapter = util.get_adapter(bufnr, true)
if not adapter then
table.insert(errors, {
lnum = 0,

View file

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

View file

@ -26,7 +26,8 @@ M.run = function(cmd, opts, callback)
if err == "" then
err = "Unknown error"
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),
})

View file

@ -1,42 +0,0 @@
---@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.LspFileMethods 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.ViewOptions 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 git? oil.GitOptions EXPERIMENTAL support for performing file operations with git
---@field float? table Configuration for the floating window in oil.open_float
---@field preview? table Configuration for the actions floating preview window
---@field progress? table Configuration for the floating progress window
---@field ssh? table Configuration for the floating SSH window
---@field keymaps_help? table Configuration for the floating keymaps help window
---@class (exact) oil.LspFileMethods
---@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 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 Sort file names in a more intuitive order for humans. Is less performant, so you may want to set to false if you work with large directories.
---@field sort? oil.SortSpec[] Sort order for the file list
---@class (exact) oil.SortSpec
---@field [1] string
---@field [2] "asc"|"desc"
---@class (exact) oil.GitOptions
---@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

View file

@ -21,50 +21,67 @@ end
---@param filename string
---@return string
M.escape_filename = function(filename)
local ret = filename:gsub("([%%#$])", "\\%1")
local ret = vim.fn.fnameescape(filename)
return ret
end
local _url_escape_chars = {
[" "] = "%20",
["$"] = "%24",
["&"] = "%26",
["`"] = "%60",
[":"] = "%3A",
["<"] = "%3C",
["="] = "%3D",
[">"] = "%3E",
["?"] = "%3F",
["["] = "%5B",
["\\"] = "%5C",
["]"] = "%5D",
["^"] = "%5E",
["{"] = "%7B",
["|"] = "%7C",
["}"] = "%7D",
["~"] = "%7E",
[""] = "%22",
[""] = "%27",
["+"] = "%2B",
[","] = "%2C",
["#"] = "%23",
["%"] = "%25",
["@"] = "%40",
["/"] = "%2F",
[";"] = "%3B",
local _url_escape_to_char = {
["20"] = " ",
["22"] = "",
["23"] = "#",
["24"] = "$",
["25"] = "%",
["26"] = "&",
["27"] = "",
["2B"] = "+",
["2C"] = ",",
["2F"] = "/",
["3A"] = ":",
["3B"] = ";",
["3C"] = "<",
["3D"] = "=",
["3E"] = ">",
["3F"] = "?",
["40"] = "@",
["5B"] = "[",
["5C"] = "\\",
["5D"] = "]",
["5E"] = "^",
["60"] = "`",
["7B"] = "{",
["7C"] = "|",
["7D"] = "}",
["7E"] = "~",
}
local _char_to_url_escape = {}
for k, v in pairs(_url_escape_to_char) do
_char_to_url_escape[v] = "%" .. k
end
-- TODO this uri escape handling is very incomplete
---@param string string
---@return string
M.url_escape = function(string)
return (string:gsub(".", _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
---@param bufnr integer
---@param silent? boolean
---@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 adapter = config.get_adapter_by_scheme(bufname)
if not adapter then
if not adapter and not silent then
vim.notify_once(
string.format("[oil] could not find adapter for buffer '%s://'", bufname),
vim.log.levels.ERROR
@ -74,34 +91,28 @@ M.get_adapter = function(bufnr)
end
---@param text string
---@param length nil|integer
---@return string
M.rpad = function(text, length)
if not length then
return text
---@param width integer|nil
---@param align oil.ColumnAlign
---@return string padded_text
---@return integer left_padding
M.pad_align = function(text, width, align)
if not width then
return text, 0
end
local textlen = vim.api.nvim_strwidth(text)
local delta = length - textlen
if delta > 0 then
return text .. string.rep(" ", delta)
else
return text
local text_width = vim.api.nvim_strwidth(text)
local total_pad = width - text_width
if total_pad <= 0 then
return text, 0
end
end
---@param text string
---@param length nil|integer
---@return string
M.lpad = function(text, length)
if not length then
return text
end
local textlen = vim.api.nvim_strwidth(text)
local delta = length - textlen
if delta > 0 then
return string.rep(" ", delta) .. text
if align == "right" then
return string.rep(" ", total_pad) .. text, total_pad
elseif align == "center" then
local left_pad = math.floor(total_pad / 2)
local right_pad = total_pad - left_pad
return string.rep(" ", left_pad) .. text .. string.rep(" ", right_pad), left_pad
else
return text
return text .. string.rep(" ", total_pad), 0
end
end
@ -157,8 +168,10 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
-- This will fail if the dest buf name already exists
local ok = pcall(vim.api.nvim_buf_set_name, src_bufnr, dest_buf_name)
if ok then
-- Renaming the buffer creates a new buffer with the old name. Find it and delete it.
vim.api.nvim_buf_delete(vim.fn.bufadd(bufname), {})
-- Renaming the buffer creates a new buffer with the old name.
-- Find it and try to delete it, but don't if the buffer is in a context
-- where Neovim doesn't allow buffer modifications.
pcall(vim.api.nvim_buf_delete, vim.fn.bufadd(bufname), {})
if altbuf and vim.api.nvim_buf_is_valid(altbuf) then
vim.fn.setreg("#", altbuf)
end
@ -195,6 +208,18 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
-- Try to delete, but don't if the buffer has changes
pcall(vim.api.nvim_buf_delete, src_bufnr, {})
end
-- Renaming a buffer won't load the undo file, so we need to do that manually
if vim.bo[dest_bufnr].undofile then
vim.api.nvim_buf_call(dest_bufnr, function()
vim.cmd.rundo({
args = { vim.fn.undofile(dest_buf_name) },
magic = { file = false, bar = false },
mods = {
emsg_silent = true,
},
})
end)
end
end)
return true
end
@ -283,11 +308,15 @@ M.split_config = function(name_or_config)
end
end
---@alias oil.ColumnAlign "left"|"center"|"right"
---@param lines oil.TextChunk[][]
---@param col_width integer[]
---@param col_align? oil.ColumnAlign[]
---@return string[]
---@return any[][] List of highlights {group, lnum, col_start, col_end}
M.render_table = function(lines, col_width)
M.render_table = function(lines, col_width, col_align)
col_align = col_align or {}
local str_lines = {}
local highlights = {}
for _, cols in ipairs(lines) do
@ -301,9 +330,12 @@ M.render_table = function(lines, col_width)
else
text = chunk
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)
local col_end = col + text:len() + 1
if hl then
if type(hl) == "table" then
-- hl has the form { [1]: hl_name, [2]: col_start, [3]: col_end }[]
@ -313,15 +345,15 @@ M.render_table = function(lines, col_width)
table.insert(highlights, {
sub_hl[1],
#str_lines,
col + sub_hl[2],
col + sub_hl[3],
col + padding + sub_hl[2],
col + padding + sub_hl[3],
})
end
else
table.insert(highlights, { hl, #str_lines, col, col_end })
table.insert(highlights, { hl, #str_lines, col + padding, col + padding + unpadded_len })
end
end
col = col_end
col = col + text:len() + 1
end
table.insert(str_lines, table.concat(pieces, " "))
end
@ -334,7 +366,12 @@ M.set_highlights = function(bufnr, highlights)
local ns = vim.api.nvim_create_namespace("Oil")
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
for _, hl in ipairs(highlights) do
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
@ -347,7 +384,8 @@ M.addslash = function(path, os_slash)
slash = "\\"
end
if not vim.endswith(path, slash) then
local endslash = path:match(slash .. "$")
if not endslash then
return path .. slash
else
return path
@ -360,6 +398,26 @@ M.is_floating_win = function(winid)
return vim.api.nvim_win_get_config(winid or 0).relative ~= ""
end
---Recalculate the window title for the current buffer
---@param winid nil|integer
---@return string
M.get_title = function(winid)
if config.float.get_win_title ~= nil then
return config.float.get_win_title(winid or 0)
end
local src_buf = vim.api.nvim_win_get_buf(winid or 0)
local title = vim.api.nvim_buf_get_name(src_buf)
local scheme, path = M.parse_url(title)
if config.adapters[scheme] == "files" then
assert(path)
local fs = require("oil.fs")
title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~")
end
return title
end
local winid_map = {}
M.add_title_to_win = function(winid, opts)
opts = opts or {}
@ -367,21 +425,10 @@ M.add_title_to_win = function(winid, opts)
if not vim.api.nvim_win_is_valid(winid) then
return
end
local function get_title()
local src_buf = vim.api.nvim_win_get_buf(winid)
local title = vim.api.nvim_buf_get_name(src_buf)
local scheme, path = M.parse_url(title)
if config.adapters[scheme] == "files" then
assert(path)
local fs = require("oil.fs")
title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~")
end
return title
end
-- HACK to force the parent window to position itself
-- See https://github.com/neovim/neovim/issues/13403
vim.cmd.redraw()
local title = 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 title_winid = winid_map[winid]
local bufnr
@ -429,7 +476,7 @@ M.add_title_to_win = function(winid, opts)
if vim.api.nvim_win_get_buf(winid) ~= winbuf then
return
end
local new_title = get_title()
local new_title = M.get_title(winid)
local new_width =
math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(new_title))
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { " " .. new_title .. " " })
@ -478,10 +525,7 @@ end
---@return oil.Adapter
---@return nil|oil.CrossAdapterAction
M.get_adapter_for_action = function(action)
local adapter = config.get_adapter_by_scheme(action.url or action.src_url)
if not adapter then
error("no adapter found")
end
local adapter = assert(config.get_adapter_by_scheme(action.url or action.src_url))
if action.dest_url then
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if adapter ~= dest_adapter then
@ -602,11 +646,7 @@ M.render_text = function(bufnr, text, opts)
pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines)
vim.bo[bufnr].modifiable = false
vim.bo[bufnr].modified = false
local ns = vim.api.nvim_create_namespace("Oil")
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
for _, hl in ipairs(highlights) do
vim.api.nvim_buf_add_highlight(bufnr, ns, unpack(hl))
end
M.set_highlights(bufnr, highlights)
end
---Run a function in the context of a full-editor window
@ -634,8 +674,12 @@ end
---@param bufnr integer
---@return boolean
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
elseif filetype ~= "" then
-- If the filetype is set and is NOT "oil", then it's not an oil buffer
return false
end
local scheme = M.parse_url(vim.api.nvim_buf_get_name(bufnr))
return config.adapters[scheme] or config.adapter_aliases[scheme]
@ -658,10 +702,17 @@ M.hack_around_termopen_autocmd = function(prev_mode)
end, 10)
end
---@param opts? {include_not_owned?: boolean}
---@return nil|integer
M.get_preview_win = function()
M.get_preview_win = function(opts)
opts = opts or {}
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
end
end
@ -736,7 +787,7 @@ end
---Send files from the current oil directory to quickfix
---based on the provided options.
---@param opts {target?: "qflist"|"loclist", mode?: "r"|"a"}
---@param opts {target?: "qflist"|"loclist", action?: "r"|"a", only_matching_search?: boolean}
M.send_to_quickfix = function(opts)
if type(opts) ~= "table" then
opts = {}
@ -750,10 +801,11 @@ M.send_to_quickfix = function(opts)
if not range then
range = { start_lnum = 1, end_lnum = vim.fn.line("$") }
end
local match_all = not opts.only_matching_search
local qf_entries = {}
for i = range.start_lnum, range.end_lnum do
local entry = oil.get_entry_on_line(0, i)
if entry and entry.type == "file" then
if entry and entry.type == "file" and (match_all or M.is_matching(entry)) then
local qf_entry = {
filename = dir .. entry.name,
lnum = 1,
@ -769,11 +821,13 @@ M.send_to_quickfix = function(opts)
end
vim.api.nvim_exec_autocmds("QuickFixCmdPre", {})
local qf_title = "oil files"
local mode = opts.mode == "a" and "a" or "r"
local action = opts.action == "a" and "a" or "r"
if opts.target == "loclist" then
vim.fn.setloclist(0, {}, mode, { title = qf_title, items = qf_entries })
vim.fn.setloclist(0, {}, action, { title = qf_title, items = qf_entries })
vim.cmd.lopen()
else
vim.fn.setqflist({}, mode, { title = qf_title, items = qf_entries })
vim.fn.setqflist({}, action, { title = qf_title, items = qf_entries })
vim.cmd.copen()
end
vim.api.nvim_exec_autocmds("QuickFixCmdPost", {})
end
@ -800,6 +854,19 @@ M.get_visual_range = function()
return { start_lnum = start_lnum, end_lnum = end_lnum }
end
---@param entry oil.Entry
---@return boolean
M.is_matching = function(entry)
-- if search highlightig is not enabled, all files are considered to match
local search_highlighting_is_off = (vim.v.hlsearch == 0)
if search_highlighting_is_off then
return true
end
local pattern = vim.fn.getreg("/")
local position_of_match = vim.fn.match(entry.name, pattern)
return position_of_match ~= -1
end
---@param bufnr integer
---@param callback fun()
M.run_after_load = function(bufnr, callback)
@ -843,7 +910,7 @@ M.get_edit_path = function(bufnr, entry, callback)
local bufname = vim.api.nvim_buf_get_name(bufnr)
local scheme, dir = M.parse_url(bufname)
local adapter = M.get_adapter(bufnr)
local adapter = M.get_adapter(bufnr, true)
assert(scheme and dir and adapter)
local url = scheme .. dir .. entry.name
@ -887,4 +954,85 @@ M.get_icon_provider = function()
end
end
---Read a buffer into a scratch buffer and apply syntactic highlighting when possible
---@param path string The path to the file to read
---@param preview_method oil.PreviewMethod
---@return nil|integer
M.read_file_to_scratch_buffer = function(path, preview_method)
local bufnr = vim.api.nvim_create_buf(false, true)
if bufnr == 0 then
return
end
vim.bo[bufnr].bufhidden = "wipe"
vim.bo[bufnr].buftype = "nofile"
local has_lines, read_res
if preview_method == "fast_scratch" then
has_lines, read_res = pcall(vim.fn.readfile, path, "", vim.o.lines)
else
has_lines, read_res = pcall(vim.fn.readfile, path)
end
local lines = has_lines and vim.split(table.concat(read_res, "\n"), "\n") or {}
local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines)
if not ok then
return
end
local ft = vim.filetype.match({ filename = path, buf = bufnr })
if ft and ft ~= "" and vim.treesitter.language.get_lang then
local lang = vim.treesitter.language.get_lang(ft)
if not pcall(vim.treesitter.start, bufnr, lang) then
vim.bo[bufnr].syntax = ft
else
end
end
-- Replace the scratch buffer with a real buffer if we enter it
vim.api.nvim_create_autocmd("BufEnter", {
desc = "oil.nvim replace scratch buffer with real buffer",
buffer = bufnr,
callback = function()
local winid = vim.api.nvim_get_current_win()
-- Have to schedule this so all the FileType, etc autocmds will fire
vim.schedule(function()
if vim.api.nvim_get_current_win() == winid then
vim.cmd.edit({ args = { path } })
-- If we're still in a preview window, make sure this buffer still gets treated as a
-- preview
if vim.wo.previewwindow then
vim.bo.bufhidden = "wipe"
vim.b.oil_preview_buffer = true
end
end
end)
end,
})
return bufnr
end
local _regcache = {}
---Check if a file matches a BufReadCmd autocmd
---@param filename string
---@return boolean
M.file_matches_bufreadcmd = function(filename)
local autocmds = vim.api.nvim_get_autocmds({
event = "BufReadCmd",
})
for _, au in ipairs(autocmds) do
local pat = _regcache[au.pattern]
if not pat then
pat = vim.fn.glob2regpat(au.pattern)
_regcache[au.pattern] = pat
end
if vim.fn.match(filename, pat) >= 0 then
return true
end
end
return false
end
return M

View file

@ -19,10 +19,16 @@ local last_cursor_entry = {}
---@param name string
---@param bufnr integer
---@return boolean
---@return boolean display
---@return boolean is_hidden Whether the file is classified as a hidden file
M.should_display = function(name, bufnr)
return not config.view_options.is_always_hidden(name, bufnr)
and (not config.view_options.is_hidden_file(name, bufnr) or config.view_options.show_hidden)
if config.view_options.is_always_hidden(name, bufnr) then
return false, true
else
local is_hidden = config.view_options.is_hidden_file(name, bufnr)
local display = config.view_options.show_hidden or not is_hidden
return display, is_hidden
end
end
---@param bufname string
@ -79,7 +85,7 @@ M.toggle_hidden = function()
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)
local any_modified = are_any_modified()
if any_modified then
@ -140,7 +146,7 @@ M.unlock_buffers = function()
buffers_locked = false
for bufnr in pairs(session) do
if vim.api.nvim_buf_is_loaded(bufnr) then
local adapter = util.get_adapter(bufnr)
local adapter = util.get_adapter(bufnr, true)
if adapter then
vim.bo[bufnr].modifiable = adapter.is_modifiable(bufnr)
end
@ -179,9 +185,18 @@ end
M.set_win_options = function()
local winid = vim.api.nvim_get_current_win()
-- work around https://github.com/neovim/neovim/pull/27422
vim.api.nvim_set_option_value("foldmethod", "manual", { scope = "local", win = winid })
for k, v in pairs(config.win_options) do
vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid })
end
if vim.wo[winid].previewwindow then -- apply preview window options last
for k, v in pairs(config.preview_win.win_options) do
vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid })
end
end
end
---Get a list of visible oil buffers and a list of hidden oil buffers
@ -242,35 +257,65 @@ local function get_first_mutable_column_col(adapter, ranges)
return min_col
end
---Force cursor to be after hidden/immutable columns
local function constrain_cursor()
if not config.constrain_cursor then
return
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 adapter = util.get_adapter(0)
if not adapter then
return
end
local cur = vim.api.nvim_win_get_cursor(0)
local line = vim.api.nvim_buf_get_lines(0, cur[1] - 1, cur[1], true)[1]
local 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 config.constrain_cursor == "editable" then
if mode == "editable" then
min_col = get_first_mutable_column_col(adapter, result.ranges)
elseif config.constrain_cursor == "name" then
elseif mode == "name" then
min_col = result.ranges.name[1]
else
error(
string.format('Unexpected value "%s" for option constrain_cursor', config.constrain_cursor)
)
error(string.format('Unexpected value "%s" for option constrain_cursor', mode))
end
if cur[2] < min_col then
vim.api.nvim_win_set_cursor(0, { cur[1], min_col })
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
@ -282,7 +327,7 @@ local function redraw_trash_virtual_text(bufnr)
return
end
local parser = require("oil.mutator.parser")
local adapter = util.get_adapter(bufnr)
local adapter = util.get_adapter(bufnr, true)
if not adapter or adapter.name ~= "trash" then
return
end
@ -333,7 +378,8 @@ M.initialize = function(bufnr)
for k, v in pairs(config.buf_options) do
vim.bo[bufnr][k] = v
end
M.set_win_options()
vim.api.nvim_buf_call(bufnr, M.set_win_options)
vim.api.nvim_create_autocmd("BufHidden", {
desc = "Delete oil buffers when no longer in use",
group = "Oil",
@ -391,7 +437,7 @@ M.initialize = function(bufnr)
callback = function()
-- For some reason the cursor bounces back to its original position,
-- so we have to defer the call
vim.schedule(constrain_cursor)
vim.schedule_wrap(constrain_cursor)(bufnr, config.constrain_cursor)
end,
})
vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, {
@ -404,15 +450,15 @@ M.initialize = function(bufnr)
return
end
constrain_cursor()
constrain_cursor(bufnr, config.constrain_cursor)
if config.preview.update_on_cursor_moved then
if config.preview_win.update_on_cursor_moved then
-- Debounce and update the preview window
if timer then
timer:again()
return
end
timer = vim.loop.new_timer()
timer = uv.new_timer()
if not timer then
return
end
@ -441,7 +487,7 @@ M.initialize = function(bufnr)
end,
})
local adapter = util.get_adapter(bufnr)
local adapter = util.get_adapter(bufnr, true)
-- Set up a watcher that will refresh the directory
if
@ -531,8 +577,9 @@ M.initialize = function(bufnr)
end
---@param adapter oil.Adapter
---@param num_entries integer
---@return fun(a: oil.InternalEntry, b: oil.InternalEntry): boolean
local function get_sort_function(adapter)
local function get_sort_function(adapter, num_entries)
local idx_funs = {}
local sort_config = config.view_options.sort
@ -554,7 +601,9 @@ local function get_sort_function(adapter)
)
end
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 })
else
vim.notify_once(
@ -565,7 +614,7 @@ local function get_sort_function(adapter)
end
return function(a, b)
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 b_val = get_sort_value(b)
if a_val ~= b_val then
@ -598,14 +647,17 @@ local function render_buffer(bufnr, opts)
jump_first = false,
})
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
return false
end
local entries = cache.list_url(bufname)
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
if opts.jump_first then
@ -616,30 +668,34 @@ local function render_buffer(bufnr, opts)
local column_defs = columns.get_supported_columns(scheme)
local line_table = {}
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
local _, conf = util.split_config(col_def)
col_align[i + 1] = conf and conf.align or "left"
end
if M.should_display("..", bufnr) then
local cols = M.format_entry_cols({ 0, "..", "directory" }, column_defs, col_width, adapter)
local cols =
M.format_entry_cols({ 0, "..", "directory" }, column_defs, col_width, adapter, true, bufnr)
table.insert(line_table, cols)
end
for _, entry in ipairs(entry_list) do
if M.should_display(entry[FIELD_NAME], bufnr) then
local cols = M.format_entry_cols(entry, column_defs, col_width, adapter)
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
M.set_last_cursor(bufname, nil)
end
end
end
local lines, highlights = util.render_table(line_table, col_width)
local lines, highlights = util.render_table(line_table, col_width, col_align)
vim.bo[bufnr].modifiable = true
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
@ -652,19 +708,23 @@ local function render_buffer(bufnr, opts)
vim.schedule(function()
for _, winid in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_is_valid(winid) and vim.api.nvim_win_get_buf(winid) == bufnr then
-- If we're not jumping to a specific lnum, use the current lnum so we can adjust the col
local lnum = jump_idx or vim.api.nvim_win_get_cursor(winid)[1]
local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]
local id_str = line:match("^/(%d+)")
local id = tonumber(id_str)
if id then
local entry = cache.get_entry_by_id(id)
if entry then
local name = entry[FIELD_NAME]
local col = line:find(name, 1, true) or (id_str:len() + 1)
vim.api.nvim_win_set_cursor(winid, { lnum, col - 1 })
if jump_idx then
local lnum = jump_idx
local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]
local id_str = line:match("^/(%d+)")
local id = tonumber(id_str)
if id then
local entry = cache.get_entry_by_id(id)
if entry then
local name = entry[FIELD_NAME]
local col = line:find(name, 1, true) or (id_str:len() + 1)
vim.api.nvim_win_set_cursor(winid, { lnum, col - 1 })
return
end
end
end
constrain_cursor(bufnr, "name")
end
end
end)
@ -672,18 +732,48 @@ local function render_buffer(bufnr, opts)
return seek_after_render_found
end
---@param name string
---@param meta? table
---@return string filename
---@return string|nil link_target
local function get_link_text(name, meta)
local link_text
if meta then
if meta.link_stat and meta.link_stat.type == "directory" then
name = name .. "/"
end
if meta.link then
link_text = "-> " .. meta.link
if meta.link_stat and meta.link_stat.type == "directory" then
link_text = util.addslash(link_text)
end
end
end
return name, link_text
end
---@private
---@param entry oil.InternalEntry
---@param column_defs table[]
---@param col_width integer[]
---@param adapter oil.Adapter
---@param is_hidden boolean
---@param bufnr integer
---@return oil.TextChunk[]
M.format_entry_cols = function(entry, column_defs, col_width, adapter)
M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden, bufnr)
local name = entry[FIELD_NAME]
local meta = entry[FIELD_META]
local hl_suffix = ""
if is_hidden then
hl_suffix = "Hidden"
end
if meta and meta.display_name then
name = meta.display_name
end
-- We can't handle newlines in filenames (and shame on you for doing that)
name = name:gsub("\n", "")
-- First put the unique ID
local cols = {}
local id_key = cache.format_id(entry[FIELD_ID])
@ -691,7 +781,7 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter)
table.insert(cols, id_key)
-- Then add all the configured columns
for i, column in ipairs(column_defs) do
local chunk = columns.render_col(adapter, column, entry)
local chunk = columns.render_col(adapter, column, entry, bufnr)
local text = type(chunk) == "table" and chunk[1] or chunk
---@cast text string
col_width[i + 1] = math.max(col_width[i + 1], vim.api.nvim_strwidth(text))
@ -699,32 +789,59 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter)
end
-- Always add the entry name at the end
local entry_type = entry[FIELD_TYPE]
if entry_type == "directory" then
table.insert(cols, { name .. "/", "OilDir" })
elseif entry_type == "socket" then
table.insert(cols, { name, "OilSocket" })
elseif entry_type == "link" then
local link_text
if meta then
if meta.link_stat and meta.link_stat.type == "directory" then
name = name .. "/"
local get_custom_hl = config.view_options.highlight_filename
local link_name, link_name_hl, link_target, link_target_hl
if get_custom_hl then
local external_entry = util.export_entry(entry)
if entry_type == "link" then
link_name, link_target = get_link_text(name, meta)
local is_orphan = not (meta and meta.link_stat)
link_name_hl = get_custom_hl(external_entry, is_hidden, false, is_orphan, bufnr)
if link_target then
link_target_hl = get_custom_hl(external_entry, is_hidden, true, is_orphan, bufnr)
end
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)
-- intentional fallthrough
else
local hl = get_custom_hl(external_entry, is_hidden, false, false, bufnr)
if hl then
-- Add the trailing / if this is a directory, this is important
if entry_type == "directory" then
name = name .. "/"
end
table.insert(cols, { name, hl })
return cols
end
end
end
table.insert(cols, { name, "OilLink" })
if link_text then
table.insert(cols, { link_text, "OilLinkTarget" })
if entry_type == "directory" then
table.insert(cols, { name .. "/", "OilDir" .. hl_suffix })
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
else
table.insert(cols, { name, "OilFile" })
table.insert(cols, { name, "OilFile" .. hl_suffix })
end
return cols
end
@ -794,7 +911,7 @@ M.render_buffer_async = function(bufnr, opts, callback)
handle_error(string.format("Could not parse oil url '%s'", bufname))
return
end
local adapter = util.get_adapter(bufnr)
local adapter = util.get_adapter(bufnr, true)
if not adapter then
handle_error(string.format("[oil] no adapter for buffer '%s'", bufname))
return
@ -813,6 +930,7 @@ M.render_buffer_async = function(bufnr, opts, callback)
vim.b[bufnr].oil_rendering = false
loading.set_loading(bufnr, false)
render_buffer(bufnr, { jump = true })
M.set_last_cursor(bufname, nil)
vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" })
vim.bo[bufnr].modifiable = not buffers_locked and adapter.is_modifiable(bufnr)
if callback then
@ -837,6 +955,7 @@ M.render_buffer_async = function(bufnr, opts, callback)
end
cache.begin_update_url(bufname)
local num_iterations = 0
adapter.list(bufname, get_used_columns(), function(err, entries, fetch_more)
loading.set_loading(bufnr, false)
if err then
@ -853,11 +972,13 @@ M.render_buffer_async = function(bufnr, opts, callback)
local now = uv.hrtime() / 1e6
local delta = now - start_ms
-- If we've been chugging for more than 40ms, go ahead and render what we have
if delta > 40 then
if (delta > 25 and num_iterations < 1) or delta > 500 then
num_iterations = num_iterations + 1
start_ms = now
vim.schedule(function()
seek_after_render_found =
render_buffer(bufnr, { jump = not seek_after_render_found, jump_first = first })
start_ms = uv.hrtime() / 1e6
end)
end
first = false

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
mkdir -p ".testenv/config/nvim"

View file

@ -110,11 +110,16 @@ class ColumnDef:
params: List["LuaParam"] = field(default_factory=list)
HL = [
UNIVERSAL = [
LuaParam(
"highlight",
"string|fun(value: string): string",
"Highlight group, or function that returns a highlight group",
),
LuaParam(
"align",
'"left"|"center"|"right"',
"Text alignment within the column",
)
]
TIME = [
@ -127,7 +132,7 @@ COL_DEFS = [
False,
True,
"The type of the entry (file, directory, link, etc)",
HL
UNIVERSAL
+ [LuaParam("icons", "table<string, string>", "Mapping of entry type to icon")],
),
ColumnDef(
@ -136,7 +141,7 @@ COL_DEFS = [
False,
False,
"An icon for the entry's type (requires nvim-web-devicons)",
HL
UNIVERSAL
+ [
LuaParam(
"default_file",
@ -151,31 +156,31 @@ COL_DEFS = [
),
],
),
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(
"permissions",
"files, ssh",
True,
False,
"Access permissions of the file",
HL + [],
UNIVERSAL + [],
),
ColumnDef(
"ctime", "files", False, True, "Change timestamp of the file", HL + TIME + []
"ctime", "files", False, True, "Change timestamp of the file", UNIVERSAL + TIME + []
),
ColumnDef(
"mtime", "files", False, True, "Last modified time of the file", 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", HL + TIME + []
"atime", "files", False, True, "Last access time of the file", UNIVERSAL + TIME + []
),
ColumnDef(
"birthtime",
"files",
"files, s3",
False,
True,
"The time the file was created",
HL + TIME + [],
UNIVERSAL + TIME + [],
),
]
@ -351,13 +356,13 @@ of being permanently deleted. You can browse the trash for a directory using
the `toggle_trash` action (bound to `g\\` by default). You can view all files
in the trash with `:Oil --trash /`.
To restore files, simply delete them from the trash and put them in the desired
destination, the same as any other file operation. If you delete files from the
trash they will be permanently deleted (purged).
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/trashspec-1.0.html
https://specifications.freedesktop.org/trash-spec/1.0/
All features should work.
Mac:
@ -366,7 +371,7 @@ Mac:
(instead of being able to see files that were trashed from a directory).
Windows:
Oil does not yet support the Windows trash. PRs are welcome!
Oil supports the Windows Recycle Bin. All features should work.
"""
)
return section

4
scripts/requirements.txt Normal file
View file

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

View file

@ -2,9 +2,9 @@ if exists("b:current_syntax")
finish
endif
syn match oilCreate /^CREATE /
syn match oilCreate /^CREATE\( BUCKET\)\? /
syn match oilMove /^ MOVE /
syn match oilDelete /^DELETE /
syn match oilDelete /^DELETE\( BUCKET\)\? /
syn match oilCopy /^ COPY /
syn match oilChange /^CHANGE /
" Trash operations

View file

@ -141,5 +141,17 @@ a.describe("Alternate buffer", function()
oil.close()
assert.equals("foo", vim.fn.expand("#"))
end)
a.it("preserves alternate when traversing to a new file", function()
vim.cmd.edit({ args = { "foo" } })
oil.open_float()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("foo", vim.fn.expand("#"))
test_util.feedkeys({ "/LICENSE<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)

View file

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

View file

@ -216,6 +216,26 @@ a.describe("mutator", function()
assert.are.same({ move1, move2 }, ordered_actions)
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()
local move = {
type = "move",

View file

@ -90,7 +90,7 @@ describe("parser", function()
local file = test_adapter.test_set("/foo/a.txt", "file")
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
local cols = view.format_entry_cols(file, {}, {}, test_adapter)
local cols = view.format_entry_cols(file, {}, {}, test_adapter, false)
local lines = util.render_table({ cols }, {})
table.insert(lines, "")
table.insert(lines, " ")

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

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

View file

@ -1,6 +1,7 @@
require("plenary.async").tests.add_to_env()
local cache = require("oil.cache")
local test_adapter = require("oil.adapters.test")
local util = require("oil.util")
local M = {}
M.reset_editor = function()
@ -33,6 +34,10 @@ local function throwiferr(err, ...)
end
end
M.oil_open = function(...)
a.wrap(require("oil").open, 3)(...)
end
M.await = function(fn, nargs, ...)
return throwiferr(a.wrap(fn, nargs)(...))
end
@ -53,6 +58,10 @@ M.wait_for_autocmd = a.wrap(function(autocmd, cb)
vim.api.nvim_create_autocmd(autocmd, opts)
end, 2)
M.wait_oil_ready = a.wrap(function(cb)
util.run_after_load(0, vim.schedule_wrap(cb))
end, 1)
---@param actions string[]
---@param timestep integer
M.feedkeys = function(actions, timestep)

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