Compare commits

...

45 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
31 changed files with 1490 additions and 317 deletions

View file

@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- master - master
- stevearc-*
pull_request: pull_request:
branches: branches:
- master - master
@ -52,8 +53,8 @@ jobs:
include: include:
- nvim_tag: v0.8.3 - nvim_tag: v0.8.3
- nvim_tag: v0.9.4 - nvim_tag: v0.9.4
- nvim_tag: v0.10.0
- nvim_tag: v0.10.4 - nvim_tag: v0.10.4
- nvim_tag: v0.11.0
name: Run tests name: Run tests
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04

View file

@ -12,6 +12,7 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94
- [Options](#options) - [Options](#options)
- [Adapters](#adapters) - [Adapters](#adapters)
- [Recipes](#recipes) - [Recipes](#recipes)
- [Third-party extensions](#third-party-extensions)
- [API](#api) - [API](#api)
- [FAQ](#faq) - [FAQ](#faq)
@ -21,7 +22,7 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94
- Neovim 0.8+ - Neovim 0.8+
- Icon provider plugin (optional) - 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 - [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) for file icons
## Installation ## Installation
@ -38,7 +39,7 @@ oil.nvim supports all the usual plugin managers
---@type oil.SetupOpts ---@type oil.SetupOpts
opts = {}, opts = {},
-- Optional dependencies -- Optional dependencies
dependencies = { { "echasnovski/mini.icons", opts = {} } }, dependencies = { { "nvim-mini/mini.icons", opts = {} } },
-- dependencies = { "nvim-tree/nvim-web-devicons" }, -- use if you prefer nvim-web-devicons -- 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 loading is not recommended because it is very tricky to make it work correctly in all situations.
lazy = false, lazy = false,
@ -203,7 +204,7 @@ require("oil").setup({
["-"] = { "actions.parent", mode = "n" }, ["-"] = { "actions.parent", mode = "n" },
["_"] = { "actions.open_cwd", mode = "n" }, ["_"] = { "actions.open_cwd", mode = "n" },
["`"] = { "actions.cd", mode = "n" }, ["`"] = { "actions.cd", mode = "n" },
["~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" }, ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
["gs"] = { "actions.change_sort", mode = "n" }, ["gs"] = { "actions.change_sort", mode = "n" },
["gx"] = "actions.open_external", ["gx"] = "actions.open_external",
["g."] = { "actions.toggle_hidden", mode = "n" }, ["g."] = { "actions.toggle_hidden", mode = "n" },
@ -241,6 +242,8 @@ require("oil").setup({
}, },
-- Extra arguments to pass to SCP when moving/copying files over SSH -- Extra arguments to pass to SCP when moving/copying files over SSH
extra_scp_args = {}, 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 -- EXPERIMENTAL support for performing file operations with git
git = { git = {
-- Return true to automatically git add/mv/rm files -- Return true to automatically git add/mv/rm files
@ -261,7 +264,7 @@ require("oil").setup({
-- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
max_width = 0, max_width = 0,
max_height = 0, max_height = 0,
border = "rounded", border = nil,
win_options = { win_options = {
winblend = 0, winblend = 0,
}, },
@ -306,7 +309,7 @@ require("oil").setup({
min_height = { 5, 0.1 }, min_height = { 5, 0.1 },
-- optionally define an integer/float for the exact height of the preview window -- optionally define an integer/float for the exact height of the preview window
height = nil, height = nil,
border = "rounded", border = nil,
win_options = { win_options = {
winblend = 0, winblend = 0,
}, },
@ -319,7 +322,7 @@ require("oil").setup({
max_height = { 10, 0.9 }, max_height = { 10, 0.9 },
min_height = { 5, 0.1 }, min_height = { 5, 0.1 },
height = nil, height = nil,
border = "rounded", border = nil,
minimized_border = "none", minimized_border = "none",
win_options = { win_options = {
winblend = 0, winblend = 0,
@ -327,11 +330,11 @@ require("oil").setup({
}, },
-- Configuration for the floating SSH window -- Configuration for the floating SSH window
ssh = { ssh = {
border = "rounded", border = nil,
}, },
-- Configuration for the floating keymaps help window -- Configuration for the floating keymaps help window
keymaps_help = { keymaps_help = {
border = "rounded", border = nil,
}, },
}) })
``` ```
@ -354,12 +357,30 @@ 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`). 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 ## Recipes
- [Toggle file detail view](doc/recipes.md#toggle-file-detail-view) - [Toggle file detail view](doc/recipes.md#toggle-file-detail-view)
- [Show CWD in the winbar](doc/recipes.md#show-cwd-in-the-winbar) - [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) - [Hide gitignored files and show git tracked hidden files](doc/recipes.md#hide-gitignored-files-and-show-git-tracked-hidden-files)
## Third-party extensions
These are plugins maintained by other authors that extend the functionality of oil.nvim.
- [oil-git-status.nvim](https://github.com/refractalize/oil-git-status.nvim) - Shows git status of files in statuscolumn
- [oil-git.nvim](https://github.com/benomahony/oil-git.nvim) - Shows git status of files with colour and symbols
- [oil-lsp-diagnostics.nvim](https://github.com/JezerM/oil-lsp-diagnostics.nvim) - Shows LSP diagnostics indicator as virtual text
## API ## API
<!-- API --> <!-- API -->
@ -373,7 +394,7 @@ Note that at the moment the ssh adapter does not support Windows machines, and i
- [toggle_hidden()](doc/api.md#toggle_hidden) - [toggle_hidden()](doc/api.md#toggle_hidden)
- [get_current_dir(bufnr)](doc/api.md#get_current_dirbufnr) - [get_current_dir(bufnr)](doc/api.md#get_current_dirbufnr)
- [open_float(dir, opts, cb)](doc/api.md#open_floatdir-opts-cb) - [open_float(dir, opts, cb)](doc/api.md#open_floatdir-opts-cb)
- [toggle_float(dir)](doc/api.md#toggle_floatdir) - [toggle_float(dir, opts, cb)](doc/api.md#toggle_floatdir-opts-cb)
- [open(dir, opts, cb)](doc/api.md#opendir-opts-cb) - [open(dir, opts, cb)](doc/api.md#opendir-opts-cb)
- [close(opts)](doc/api.md#closeopts) - [close(opts)](doc/api.md#closeopts)
- [open_preview(opts, callback)](doc/api.md#open_previewopts-callback) - [open_preview(opts, callback)](doc/api.md#open_previewopts-callback)
@ -400,7 +421,7 @@ Plus, I think it's pretty slick ;)
- You like to use a netrw-like view to browse directories (as opposed to a file tree) - You like to use a netrw-like view to browse directories (as opposed to a file tree)
- AND you want to be able to edit your filesystem like a buffer - AND you want to be able to edit your filesystem like a buffer
- AND you want to perform cross-directory actions. AFAIK there is no other plugin that does this. (update: [mini.files](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-files.md) also offers this functionality) - AND you want to perform cross-directory actions. AFAIK there is no other plugin that does this. (update: [mini.files](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-files.md) also offers this functionality)
If you don't need those features specifically, check out the alternatives listed below If you don't need those features specifically, check out the alternatives listed below
@ -416,7 +437,7 @@ If you don't need those features specifically, check out the alternatives listed
**A:** **A:**
- [mini.files](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-files.md): A newer plugin that also supports cross-directory filesystem-as-buffer edits. It utilizes a unique column view. - [mini.files](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-files.md): A newer plugin that also supports cross-directory filesystem-as-buffer edits. It utilizes a unique column view.
- [vim-vinegar](https://github.com/tpope/vim-vinegar): The granddaddy. This made me fall in love with single-directory file browsing. I stopped using it when I encountered netrw bugs and performance issues. - [vim-vinegar](https://github.com/tpope/vim-vinegar): The granddaddy. This made me fall in love with single-directory file browsing. I stopped using it when I encountered netrw bugs and performance issues.
- [defx.nvim](https://github.com/Shougo/defx.nvim): What I switched to after vim-vinegar. Much more flexible and performant, but requires python and the API is a little hard to work with. - [defx.nvim](https://github.com/Shougo/defx.nvim): What I switched to after vim-vinegar. Much more flexible and performant, but requires python and the API is a little hard to work with.
- [dirbuf.nvim](https://github.com/elihunter173/dirbuf.nvim): The first plugin I encountered that let you edit the filesystem like a buffer. Never used it because it [can't do cross-directory edits](https://github.com/elihunter173/dirbuf.nvim/issues/7). - [dirbuf.nvim](https://github.com/elihunter173/dirbuf.nvim): The first plugin I encountered that let you edit the filesystem like a buffer. Never used it because it [can't do cross-directory edits](https://github.com/elihunter173/dirbuf.nvim/issues/7).

View file

@ -11,7 +11,7 @@
- [toggle_hidden()](#toggle_hidden) - [toggle_hidden()](#toggle_hidden)
- [get_current_dir(bufnr)](#get_current_dirbufnr) - [get_current_dir(bufnr)](#get_current_dirbufnr)
- [open_float(dir, opts, cb)](#open_floatdir-opts-cb) - [open_float(dir, opts, cb)](#open_floatdir-opts-cb)
- [toggle_float(dir)](#toggle_floatdir) - [toggle_float(dir, opts, cb)](#toggle_floatdir-opts-cb)
- [open(dir, opts, cb)](#opendir-opts-cb) - [open(dir, opts, cb)](#opendir-opts-cb)
- [close(opts)](#closeopts) - [close(opts)](#closeopts)
- [open_preview(opts, callback)](#open_previewopts-callback) - [open_preview(opts, callback)](#open_previewopts-callback)
@ -107,14 +107,20 @@ Open oil browser in a floating window
| >>split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | | >>split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier |
| cb | `nil\|fun()` | Called after the oil buffer is ready | | cb | `nil\|fun()` | Called after the oil buffer is ready |
## toggle_float(dir) ## toggle_float(dir, opts, cb)
`toggle_float(dir)` \ `toggle_float(dir, opts, cb)` \
Open oil browser in a floating window, or close it if open Open oil browser in a floating window, or close it if open
| Param | Type | Desc | | Param | Type | Desc |
| ----- | ------------- | ------------------------------------------------------------------------------------------- | | ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | | dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file |
| opts | `nil\|oil.OpenOpts` | |
| >preview | `nil\|oil.OpenPreviewOpts` | When present, open the preview window after opening oil |
| >>vertical | `nil\|boolean` | Open the buffer in a vertical split |
| >>horizontal | `nil\|boolean` | Open the buffer in a horizontal split |
| >>split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier |
| cb | `nil\|fun()` | Called after the oil buffer is ready |
## open(dir, opts, cb) ## open(dir, opts, cb)
@ -160,13 +166,14 @@ Preview the entry under the cursor in a split
Select the entry under the cursor Select the entry under the cursor
| Param | Type | Desc | | Param | Type | Desc |
| ----------- | ------------------------------------------------------- | ---------------------------------------------------- | | ----------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| opts | `nil\|oil.SelectOpts` | | | opts | `nil\|oil.SelectOpts` | |
| >vertical | `nil\|boolean` | Open the buffer in a vertical split | | >vertical | `nil\|boolean` | Open the buffer in a vertical split |
| >horizontal | `nil\|boolean` | Open the buffer in a horizontal split | | >horizontal | `nil\|boolean` | Open the buffer in a horizontal split |
| >split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | | >split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier |
| >tab | `nil\|boolean` | Open the buffer in a new tab | | >tab | `nil\|boolean` | Open the buffer in a new tab |
| >close | `nil\|boolean` | Close the original oil buffer once selection is made | | >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 | | callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened |
## save(opts, cb) ## save(opts, cb)

View file

@ -86,7 +86,7 @@ CONFIG *oil-confi
["-"] = { "actions.parent", mode = "n" }, ["-"] = { "actions.parent", mode = "n" },
["_"] = { "actions.open_cwd", mode = "n" }, ["_"] = { "actions.open_cwd", mode = "n" },
["`"] = { "actions.cd", mode = "n" }, ["`"] = { "actions.cd", mode = "n" },
["~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" }, ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
["gs"] = { "actions.change_sort", mode = "n" }, ["gs"] = { "actions.change_sort", mode = "n" },
["gx"] = "actions.open_external", ["gx"] = "actions.open_external",
["g."] = { "actions.toggle_hidden", mode = "n" }, ["g."] = { "actions.toggle_hidden", mode = "n" },
@ -124,6 +124,8 @@ CONFIG *oil-confi
}, },
-- Extra arguments to pass to SCP when moving/copying files over SSH -- Extra arguments to pass to SCP when moving/copying files over SSH
extra_scp_args = {}, 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 -- EXPERIMENTAL support for performing file operations with git
git = { git = {
-- Return true to automatically git add/mv/rm files -- Return true to automatically git add/mv/rm files
@ -144,7 +146,7 @@ CONFIG *oil-confi
-- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
max_width = 0, max_width = 0,
max_height = 0, max_height = 0,
border = "rounded", border = nil,
win_options = { win_options = {
winblend = 0, winblend = 0,
}, },
@ -189,7 +191,7 @@ CONFIG *oil-confi
min_height = { 5, 0.1 }, min_height = { 5, 0.1 },
-- optionally define an integer/float for the exact height of the preview window -- optionally define an integer/float for the exact height of the preview window
height = nil, height = nil,
border = "rounded", border = nil,
win_options = { win_options = {
winblend = 0, winblend = 0,
}, },
@ -202,7 +204,7 @@ CONFIG *oil-confi
max_height = { 10, 0.9 }, max_height = { 10, 0.9 },
min_height = { 5, 0.1 }, min_height = { 5, 0.1 },
height = nil, height = nil,
border = "rounded", border = nil,
minimized_border = "none", minimized_border = "none",
win_options = { win_options = {
winblend = 0, winblend = 0,
@ -210,11 +212,11 @@ CONFIG *oil-confi
}, },
-- Configuration for the floating SSH window -- Configuration for the floating SSH window
ssh = { ssh = {
border = "rounded", border = nil,
}, },
-- Configuration for the floating keymaps help window -- Configuration for the floating keymaps help window
keymaps_help = { keymaps_help = {
border = "rounded", border = nil,
}, },
}) })
< <
@ -319,12 +321,20 @@ open_float({dir}, {opts}, {cb}) *oil.open_floa
plit modifier plit modifier
{cb} `nil|fun()` Called after the oil buffer is ready {cb} `nil|fun()` Called after the oil buffer is ready
toggle_float({dir}) *oil.toggle_float* toggle_float({dir}, {opts}, {cb}) *oil.toggle_float*
Open oil browser in a floating window, or close it if open Open oil browser in a floating window, or close it if open
Parameters: Parameters:
{dir} `nil|string` When nil, open the parent of the current buffer, or the {dir} `nil|string` When nil, open the parent of the current buffer, or
cwd if current buffer is not a file the cwd if current buffer is not a file
{opts} `nil|oil.OpenOpts`
{preview} `nil|oil.OpenPreviewOpts` When present, open the preview
window after opening oil
{vertical} `nil|boolean` Open the buffer in a vertical split
{horizontal} `nil|boolean` Open the buffer in a horizontal split
{split} `nil|"aboveleft"|"belowright"|"topleft"|"botright"` S
plit modifier
{cb} `nil|fun()` Called after the oil buffer is ready
open({dir}, {opts}, {cb}) *oil.open* open({dir}, {opts}, {cb}) *oil.open*
Open oil browser for a directory Open oil browser for a directory
@ -373,6 +383,10 @@ select({opts}, {callback}) *oil.selec
{tab} `nil|boolean` Open the buffer in a new tab {tab} `nil|boolean` Open the buffer in a new tab
{close} `nil|boolean` Close the original oil buffer once {close} `nil|boolean` Close the original oil buffer once
selection is made 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 {callback} `nil|fun(err: nil|string)` Called once all entries have been
opened opened
@ -408,6 +422,7 @@ type *column-typ
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{icons} `table<string, string>` Mapping of entry type to icon {icons} `table<string, string>` Mapping of entry type to icon
icon *column-icon* icon *column-icon*
@ -417,6 +432,7 @@ icon *column-ico
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{default_file} `string` Fallback icon for files when nvim-web-devicons {default_file} `string` Fallback icon for files when nvim-web-devicons
returns nil returns nil
{directory} `string` Icon for directories {directory} `string` Icon for directories
@ -424,13 +440,14 @@ icon *column-ico
the icon the icon
size *column-size* size *column-size*
Adapters: files, ssh Adapters: files, ssh, s3
Sortable: this column can be used in view_props.sort Sortable: this column can be used in view_props.sort
The size of the file The size of the file
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
permissions *column-permissions* permissions *column-permissions*
Adapters: files, ssh Adapters: files, ssh
@ -440,6 +457,7 @@ permissions *column-permission
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
ctime *column-ctime* ctime *column-ctime*
Adapters: files Adapters: files
@ -449,6 +467,7 @@ ctime *column-ctim
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{format} `string` Format string (see :help strftime) {format} `string` Format string (see :help strftime)
mtime *column-mtime* mtime *column-mtime*
@ -459,6 +478,7 @@ mtime *column-mtim
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{format} `string` Format string (see :help strftime) {format} `string` Format string (see :help strftime)
atime *column-atime* atime *column-atime*
@ -469,16 +489,18 @@ atime *column-atim
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{format} `string` Format string (see :help strftime) {format} `string` Format string (see :help strftime)
birthtime *column-birthtime* birthtime *column-birthtime*
Adapters: files Adapters: files, s3
Sortable: this column can be used in view_props.sort Sortable: this column can be used in view_props.sort
The time the file was created The time the file was created
Parameters: Parameters:
{highlight} `string|fun(value: string): string` Highlight group, or {highlight} `string|fun(value: string): string` Highlight group, or
function that returns a highlight group function that returns a highlight group
{align} `"left"|"center"|"right"` Text alignment within the column
{format} `string` Format string (see :help strftime) {format} `string` Format string (see :help strftime)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -545,6 +567,9 @@ close *actions.clos
Parameters: Parameters:
{exit_if_last_buf} `boolean` Exit vim if oil is closed as the last buffer {exit_if_last_buf} `boolean` Exit vim if oil is closed as the last buffer
copy_to_system_clipboard *actions.copy_to_system_clipboard*
Copy the entry under the cursor to the system clipboard
open_cmdline *actions.open_cmdline* open_cmdline *actions.open_cmdline*
Open vim cmdline with current entry as an argument Open vim cmdline with current entry as an argument
@ -565,6 +590,12 @@ open_terminal *actions.open_termina
parent *actions.parent* parent *actions.parent*
Navigate to the parent path Navigate to the parent path
paste_from_system_clipboard *actions.paste_from_system_clipboard*
Paste the system clipboard into the current oil directory
Parameters:
{delete_original} `boolean` Delete the original file after copying
preview *actions.preview* preview *actions.preview*
Open the entry under the cursor in a preview window, or close the preview Open the entry under the cursor in a preview window, or close the preview
window if already open window if already open
@ -578,6 +609,12 @@ preview *actions.previe
preview_scroll_down *actions.preview_scroll_down* preview_scroll_down *actions.preview_scroll_down*
Scroll down in the preview window Scroll down in the preview window
preview_scroll_left *actions.preview_scroll_left*
Scroll left in the preview window
preview_scroll_right *actions.preview_scroll_right*
Scroll right in the preview window
preview_scroll_up *actions.preview_scroll_up* preview_scroll_up *actions.preview_scroll_up*
Scroll up in the preview window Scroll up in the preview window
@ -631,6 +668,9 @@ yank_entry *actions.yank_entr
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
HIGHLIGHTS *oil-highlights* HIGHLIGHTS *oil-highlights*
OilEmpty *hl-OilEmpty*
Empty column values
OilHidden *hl-OilHidden* OilHidden *hl-OilHidden*
Hidden entry in an oil buffer Hidden entry in an oil buffer

View file

@ -136,6 +136,30 @@ M.preview_scroll_up = {
end, end,
} }
M.preview_scroll_left = {
desc = "Scroll left in the preview window",
callback = function()
local winid = util.get_preview_win()
if winid then
vim.api.nvim_win_call(winid, function()
vim.cmd.normal({ "zH", bang = true })
end)
end
end,
}
M.preview_scroll_right = {
desc = "Scroll right in the preview window",
callback = function()
local winid = util.get_preview_win()
if winid then
vim.api.nvim_win_call(winid, function()
vim.cmd.normal({ "zL", bang = true })
end)
end
end,
}
M.parent = { M.parent = {
desc = "Navigate to the parent path", desc = "Navigate to the parent path",
callback = oil.open, callback = oil.open,
@ -229,13 +253,24 @@ M.open_terminal = {
assert(dir, "Oil buffer with files adapter must have current directory") assert(dir, "Oil buffer with files adapter must have current directory")
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
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 }) vim.fn.termopen(vim.o.shell, { cwd = dir })
end
elseif adapter.name == "ssh" then elseif adapter.name == "ssh" then
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
local url = require("oil.adapters.ssh").parse_url(bufname) local url = require("oil.adapters.ssh").parse_url(bufname)
local cmd = require("oil.adapters.ssh.connection").create_ssh_command(url) local cmd = require("oil.adapters.ssh.connection").create_ssh_command(url)
local term_id = vim.fn.termopen(cmd) local term_id
if vim.fn.has("nvim-0.11") == 1 then
term_id = vim.fn.jobstart(cmd, { term = true })
else
---@diagnostic disable-next-line: deprecated
term_id = vim.fn.termopen(cmd)
end
if term_id then if term_id then
vim.api.nvim_chan_send(term_id, string.format("cd %s\n", url.path)) vim.api.nvim_chan_send(term_id, string.format("cd %s\n", url.path))
end end
@ -418,6 +453,26 @@ M.copy_entry_filename = {
end, end,
} }
M.copy_to_system_clipboard = {
desc = "Copy the entry under the cursor to the system clipboard",
callback = function()
require("oil.clipboard").copy_to_system_clipboard()
end,
}
M.paste_from_system_clipboard = {
desc = "Paste the system clipboard into the current oil directory",
callback = function(opts)
require("oil.clipboard").paste_from_system_clipboard(opts and opts.delete_original)
end,
parameters = {
delete_original = {
type = "boolean",
desc = "Delete the original file after copying",
},
},
}
M.open_cmdline_dir = { M.open_cmdline_dir = {
desc = "Open vim cmdline with current directory as an argument", desc = "Open vim cmdline with current directory as an argument",
deprecated = true, deprecated = true,

View file

@ -6,7 +6,6 @@ local fs = require("oil.fs")
local git = require("oil.git") local git = require("oil.git")
local log = require("oil.log") local log = require("oil.log")
local permissions = require("oil.adapters.files.permissions") local permissions = require("oil.adapters.files.permissions")
local trash = require("oil.adapters.files.trash")
local util = require("oil.util") local util = require("oil.util")
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
@ -183,7 +182,17 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
-- Replace placeholders with a pattern that matches non-space characters (e.g. %H -> %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 -- and whitespace with a pattern that matches any amount of whitespace
-- e.g. "%b %d %Y" -> "%S+%s+%S+%s+%S+" -- e.g. "%b %d %Y" -> "%S+%s+%S+%s+%S+"
pattern = fmt:gsub("%%.", "%%S+"):gsub("%s+", "%%s+") pattern = fmt
:gsub("%%.", "%%S+")
:gsub("%s+", "%%s+")
-- escape `()[]` because those are special characters in Lua patterns
:gsub(
"%(",
"%%("
)
:gsub("%)", "%%)")
:gsub("%[", "%%[")
:gsub("%]", "%%]")
else else
pattern = "%S+%s+%d+%s+%d%d:?%d%d" pattern = "%S+%s+%d+%s+%d%d:?%d%d"
end end
@ -267,7 +276,7 @@ M.normalize_url = function(url, callback)
local norm_path = util.addslash(fs.os_to_posix_path(realpath)) local norm_path = util.addslash(fs.os_to_posix_path(realpath))
callback(scheme .. norm_path) callback(scheme .. norm_path)
else else
callback(realpath) callback(vim.fn.fnamemodify(realpath, ":."))
end end
end) end)
) )
@ -530,7 +539,7 @@ M.render_action = function(action)
return string.format("DELETE %s", short_path) return string.format("DELETE %s", short_path)
end end
elseif action.type == "move" or action.type == "copy" then elseif action.type == "move" or action.type == "copy" then
local dest_adapter = config.get_adapter_by_scheme(action.dest_url) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if dest_adapter == M then if dest_adapter == M then
local _, src_path = util.parse_url(action.src_url) local _, src_path = util.parse_url(action.src_url)
assert(src_path) assert(src_path)
@ -610,20 +619,12 @@ M.perform_action = function(action, cb)
end end
if config.delete_to_trash then 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) require("oil.adapters.trash").delete_to_trash(path, cb)
end
else else
fs.recursive_delete(action.entry_type, path, cb) fs.recursive_delete(action.entry_type, path, cb)
end end
elseif action.type == "move" then elseif action.type == "move" then
local dest_adapter = config.get_adapter_by_scheme(action.dest_url) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if dest_adapter == M then if dest_adapter == M then
local _, src_path = util.parse_url(action.src_url) local _, src_path = util.parse_url(action.src_url)
assert(src_path) assert(src_path)
@ -641,7 +642,7 @@ M.perform_action = function(action, cb)
cb("files adapter doesn't support cross-adapter move") cb("files adapter doesn't support cross-adapter move")
end end
elseif action.type == "copy" then elseif action.type == "copy" then
local dest_adapter = config.get_adapter_by_scheme(action.dest_url) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if dest_adapter == M then if dest_adapter == M then
local _, src_path = util.parse_url(action.src_url) local _, src_path = util.parse_url(action.src_url)
assert(src_path) assert(src_path)

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

@ -303,8 +303,8 @@ M.perform_action = function(action, cb)
local conn = get_connection(action.url) local conn = get_connection(action.url)
conn:rm(res.path, cb) conn:rm(res.path, cb)
elseif action.type == "move" then elseif action.type == "move" then
local src_adapter = config.get_adapter_by_scheme(action.src_url) local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = config.get_adapter_by_scheme(action.dest_url) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter == M and dest_adapter == M then if src_adapter == M and dest_adapter == M then
local src_res = M.parse_url(action.src_url) local src_res = M.parse_url(action.src_url)
local dest_res = M.parse_url(action.dest_url) local dest_res = M.parse_url(action.dest_url)
@ -324,8 +324,8 @@ M.perform_action = function(action, cb)
cb("We should never attempt to move across adapters") cb("We should never attempt to move across adapters")
end end
elseif action.type == "copy" then elseif action.type == "copy" then
local src_adapter = config.get_adapter_by_scheme(action.src_url) local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = config.get_adapter_by_scheme(action.dest_url) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter == M and dest_adapter == M then if src_adapter == M and dest_adapter == M then
local src_res = M.parse_url(action.src_url) local src_res = M.parse_url(action.src_url)
local dest_res = M.parse_url(action.dest_url) local dest_res = M.parse_url(action.dest_url)

View file

@ -42,10 +42,17 @@ local function parse_ls_line(line)
local name, size, date, major, minor local name, size, date, major, minor
if typechar == "c" or typechar == "b" then if typechar == "c" or typechar == "b" then
major, minor, date, name = rem:match("^(%d+)%s*,%s*(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)") major, minor, date, name = rem:match("^(%d+)%s*,%s*(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)")
if name == nil then
major, minor, date, name =
rem:match("^(%d+)%s*,%s*(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)")
end
meta.major = tonumber(major) meta.major = tonumber(major)
meta.minor = tonumber(minor) meta.minor = tonumber(minor)
else else
size, date, name = rem:match("^(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)") size, date, name = rem:match("^(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)")
if name == nil then
size, date, name = rem:match("^(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)")
end
meta.size = tonumber(size) meta.size = tonumber(size)
end end
meta.iso_modified_date = date meta.iso_modified_date = date

View file

@ -447,7 +447,7 @@ M.render_action = function(action)
local entry = assert(cache.get_entry_by_url(action.url)) local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
---@type oil.TrashInfo ---@type oil.TrashInfo
local trash_info = meta and meta.trash_info local trash_info = assert(meta).trash_info
local short_path = fs.shorten_path(trash_info.original_path) local short_path = fs.shorten_path(trash_info.original_path)
return string.format(" PURGE %s", short_path) return string.format(" PURGE %s", short_path)
elseif action.type == "move" then elseif action.type == "move" then
@ -561,7 +561,7 @@ M.perform_action = function(action, cb)
local entry = assert(cache.get_entry_by_url(action.url)) local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
---@type oil.TrashInfo ---@type oil.TrashInfo
local trash_info = meta and meta.trash_info local trash_info = assert(meta).trash_info
purge(trash_info, cb) purge(trash_info, cb)
elseif action.type == "move" then elseif action.type == "move" then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
@ -576,7 +576,7 @@ M.perform_action = function(action, cb)
local entry = assert(cache.get_entry_by_url(action.src_url)) local entry = assert(cache.get_entry_by_url(action.src_url))
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
---@type oil.TrashInfo ---@type oil.TrashInfo
local trash_info = meta and meta.trash_info local trash_info = assert(meta).trash_info
fs.recursive_move(action.entry_type, trash_info.trash_file, dest_path, function(err) fs.recursive_move(action.entry_type, trash_info.trash_file, dest_path, function(err)
if err then if err then
return cb(err) return cb(err)
@ -607,7 +607,7 @@ M.perform_action = function(action, cb)
local entry = assert(cache.get_entry_by_url(action.src_url)) local entry = assert(cache.get_entry_by_url(action.src_url))
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
---@type oil.TrashInfo ---@type oil.TrashInfo
local trash_info = meta and meta.trash_info local trash_info = assert(meta).trash_info
fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb) fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb)
else else
error("Must be moving files into or out of trash") error("Must be moving files into or out of trash")

View file

@ -37,10 +37,10 @@ local win_addslash = function(path)
end end
---@class oil.WindowsTrashInfo ---@class oil.WindowsTrashInfo
---@field trash_file string? ---@field trash_file string
---@field original_path string? ---@field original_path string
---@field deletion_date string? ---@field deletion_date integer
---@field info_file string? ---@field info_file? string
---@param url string ---@param url string
---@param column_defs string[] ---@param column_defs string[]
@ -96,6 +96,7 @@ M.list = function(url, column_defs, cb)
end end
cache_entry[FIELD_META] = { cache_entry[FIELD_META] = {
stat = nil, stat = nil,
---@type oil.WindowsTrashInfo
trash_info = { trash_info = {
trash_file = entry.Path, trash_file = entry.Path,
original_path = entry.OriginalPath, original_path = entry.OriginalPath,
@ -265,7 +266,7 @@ M.render_action = function(action)
local entry = assert(cache.get_entry_by_url(action.url)) local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
---@type oil.WindowsTrashInfo ---@type oil.WindowsTrashInfo
local trash_info = meta and meta.trash_info local trash_info = assert(meta).trash_info
local short_path = fs.shorten_path(trash_info.original_path) local short_path = fs.shorten_path(trash_info.original_path)
return string.format(" PURGE %s", short_path) return string.format(" PURGE %s", short_path)
elseif action.type == "move" then elseif action.type == "move" then

View file

@ -28,6 +28,16 @@ end
---@param init_command? string ---@param init_command? string
function PowershellConnection:_init(init_command) 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 -- 65001 is the UTF-8 codepage
-- powershell needs to be launched with the UTF-8 codepage to use it for both stdin and stdout -- powershell needs to be launched with the UTF-8 codepage to use it for both stdin and stdout
local jid = vim.fn.jobstart({ local jid = vim.fn.jobstart({
@ -57,6 +67,7 @@ function PowershellConnection:_init(init_command)
end end
end, end,
}) })
vim.o.shellslash = saved_shellslash
if jid == 0 then if jid == 0 then
self:_set_error("passed invalid arguments to 'powershell'") self:_set_error("passed invalid arguments to 'powershell'")

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

@ -53,7 +53,7 @@ M.get_supported_columns = function(adapter_or_scheme)
return ret return ret
end end
local EMPTY = { "-", "Comment" } local EMPTY = { "-", "OilEmpty" }
M.EMPTY = EMPTY M.EMPTY = EMPTY
@ -98,13 +98,13 @@ end
M.parse_col = function(adapter, line, col_def) M.parse_col = function(adapter, line, col_def)
local name, conf = util.split_config(col_def) local name, conf = util.split_config(col_def)
-- If rendering failed, there will just be a "-" -- If rendering failed, there will just be a "-"
local empty_col, rem = line:match("^(-%s+)(.*)$") local empty_col, rem = line:match("^%s*(-%s+)(.*)$")
if empty_col then if empty_col then
return nil, rem return nil, rem
end end
local column = M.get_column(adapter, name) local column = M.get_column(adapter, name)
if column then if column then
return column.parse(line, conf) return column.parse(line:gsub("^%s+", ""), conf)
end end
end end
@ -200,7 +200,7 @@ local function is_entry_directory(entry)
return true return true
elseif type == "link" then elseif type == "link" then
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
return meta and meta.link_stat and meta.link_stat.type == "directory" return (meta and meta.link_stat and meta.link_stat.type == "directory") == true
else else
return false return false
end end
@ -228,8 +228,8 @@ M.register("type", {
end, end,
}) })
local function pad_number(int) local function adjust_number(int)
return string.format("%012d", int) return string.format("%03d%s", #int, int)
end end
M.register("name", { M.register("name", {
@ -256,14 +256,16 @@ M.register("name", {
end end
end end
else 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 if config.view_options.case_insensitive then
return function(entry) name = name:lower()
return entry[FIELD_NAME]:gsub("%d+", pad_number):lower()
end end
else memo[entry] = name
return function(entry)
return entry[FIELD_NAME]:gsub("%d+", pad_number)
end end
return memo[entry]
end end
end end
end, end,

View file

@ -69,7 +69,7 @@ local default_config = {
["-"] = { "actions.parent", mode = "n" }, ["-"] = { "actions.parent", mode = "n" },
["_"] = { "actions.open_cwd", mode = "n" }, ["_"] = { "actions.open_cwd", mode = "n" },
["`"] = { "actions.cd", mode = "n" }, ["`"] = { "actions.cd", mode = "n" },
["~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" }, ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
["gs"] = { "actions.change_sort", mode = "n" }, ["gs"] = { "actions.change_sort", mode = "n" },
["gx"] = "actions.open_external", ["gx"] = "actions.open_external",
["g."] = { "actions.toggle_hidden", mode = "n" }, ["g."] = { "actions.toggle_hidden", mode = "n" },
@ -107,6 +107,8 @@ local default_config = {
}, },
-- Extra arguments to pass to SCP when moving/copying files over SSH -- Extra arguments to pass to SCP when moving/copying files over SSH
extra_scp_args = {}, 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 -- EXPERIMENTAL support for performing file operations with git
git = { git = {
-- Return true to automatically git add/mv/rm files -- Return true to automatically git add/mv/rm files
@ -127,7 +129,7 @@ local default_config = {
-- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
max_width = 0, max_width = 0,
max_height = 0, max_height = 0,
border = "rounded", border = nil,
win_options = { win_options = {
winblend = 0, winblend = 0,
}, },
@ -172,7 +174,7 @@ local default_config = {
min_height = { 5, 0.1 }, min_height = { 5, 0.1 },
-- optionally define an integer/float for the exact height of the preview window -- optionally define an integer/float for the exact height of the preview window
height = nil, height = nil,
border = "rounded", border = nil,
win_options = { win_options = {
winblend = 0, winblend = 0,
}, },
@ -185,7 +187,7 @@ local default_config = {
max_height = { 10, 0.9 }, max_height = { 10, 0.9 },
min_height = { 5, 0.1 }, min_height = { 5, 0.1 },
height = nil, height = nil,
border = "rounded", border = nil,
minimized_border = "none", minimized_border = "none",
win_options = { win_options = {
winblend = 0, winblend = 0,
@ -193,20 +195,25 @@ local default_config = {
}, },
-- Configuration for the floating SSH window -- Configuration for the floating SSH window
ssh = { ssh = {
border = "rounded", border = nil,
}, },
-- Configuration for the floating keymaps help window -- Configuration for the floating keymaps help window
keymaps_help = { keymaps_help = {
border = "rounded", border = nil,
}, },
} }
-- The adapter API hasn't really stabilized yet. We're not ready to advertise or encourage people to -- The adapter API hasn't really stabilized yet. We're not ready to advertise or encourage people to
-- write their own adapters, and so there's no real reason to edit these config options. For that -- write their own adapters, and so there's no real reason to edit these config options. For that
-- reason, I'm taking them out of the section above so they won't show up in the autogen docs. -- reason, I'm taking them out of the section above so they won't show up in the autogen docs.
-- not "oil-s3://" on older neovim versions, since it doesn't open buffers correctly with a number
-- in the name
local oil_s3_string = vim.fn.has("nvim-0.12") == 1 and "oil-s3://" or "oil-sss://"
default_config.adapters = { default_config.adapters = {
["oil://"] = "files", ["oil://"] = "files",
["oil-ssh://"] = "ssh", ["oil-ssh://"] = "ssh",
[oil_s3_string] = "s3",
["oil-trash://"] = "trash", ["oil-trash://"] = "trash",
} }
default_config.adapter_aliases = {} default_config.adapter_aliases = {}
@ -217,7 +224,6 @@ default_config.view_options.highlight_filename = nil
---@class oil.Config ---@class oil.Config
---@field adapters table<string, string> Hidden from SetupOpts ---@field adapters table<string, string> Hidden from SetupOpts
---@field adapter_aliases table<string, string> Hidden from SetupOpts ---@field adapter_aliases table<string, string> Hidden from SetupOpts
---@field trash_command? string Deprecated option that we should clean up soon
---@field silence_scp_warning? boolean Undocumented option ---@field silence_scp_warning? boolean Undocumented option
---@field default_file_explorer boolean ---@field default_file_explorer boolean
---@field columns oil.ColumnSpec[] ---@field columns oil.ColumnSpec[]
@ -234,6 +240,7 @@ default_config.view_options.highlight_filename = nil
---@field use_default_keymaps boolean ---@field use_default_keymaps boolean
---@field view_options oil.ViewOptions ---@field view_options oil.ViewOptions
---@field extra_scp_args string[] ---@field extra_scp_args string[]
---@field extra_s3_args string[]
---@field git oil.GitOptions ---@field git oil.GitOptions
---@field float oil.FloatWindowConfig ---@field float oil.FloatWindowConfig
---@field preview_win oil.PreviewWindowConfig ---@field preview_win oil.PreviewWindowConfig
@ -262,6 +269,7 @@ local M = {}
---@field use_default_keymaps? boolean Set to false to disable all of the above keymaps ---@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 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_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 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 float? oil.SetupFloatWindowConfig Configuration for the floating window in oil.open_float
---@field preview_win? oil.SetupPreviewWindowConfig Configuration for the file preview window ---@field preview_win? oil.SetupPreviewWindowConfig Configuration for the file preview window
@ -394,13 +402,6 @@ local M = {}
M.setup = function(opts) M.setup = function(opts)
opts = opts or {} opts = opts or {}
if opts.trash_command then
vim.notify(
"[oil.nvim] trash_command is deprecated. Use built-in trash functionality instead (:help oil-trash).\nCompatibility will be removed on 2025-06-01.",
vim.log.levels.WARN
)
end
local new_conf = vim.tbl_deep_extend("keep", opts, default_config) local new_conf = vim.tbl_deep_extend("keep", opts, default_config)
if not new_conf.use_default_keymaps then if not new_conf.use_default_keymaps then
new_conf.keymaps = opts.keymaps or {} new_conf.keymaps = opts.keymaps or {}
@ -412,6 +413,17 @@ M.setup = function(opts)
end end
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'. -- Backwards compatibility. We renamed the 'preview' window config to be called 'confirmation'.
if opts.preview and not opts.confirmation then if opts.preview and not opts.confirmation then
new_conf.confirmation = vim.tbl_deep_extend("keep", opts.preview, default_config.confirmation) new_conf.confirmation = vim.tbl_deep_extend("keep", opts.preview, default_config.confirmation)
@ -464,10 +476,6 @@ M.get_adapter_by_scheme = function(scheme)
if adapter == nil then if adapter == nil then
local name = M.adapters[scheme] local name = M.adapters[scheme]
if not name then if not name then
vim.notify(
string.format("Could not find oil adapter for scheme '%s'", scheme),
vim.log.levels.ERROR
)
return nil return nil
end end
local ok local ok
@ -478,7 +486,6 @@ M.get_adapter_by_scheme = function(scheme)
else else
M._adapter_by_scheme[scheme] = false M._adapter_by_scheme[scheme] = false
adapter = false adapter = false
vim.notify(string.format("Could not find oil adapter '%s'", name), vim.log.levels.ERROR)
end end
end end
if adapter then if adapter then

View file

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

View file

@ -218,7 +218,7 @@ M.recursive_delete = function(entry_type, path, cb)
local waiting = #entries local waiting = #entries
local complete local complete
complete = function(err2) complete = function(err2)
if err then if err2 then
complete = function() end complete = function() end
return inner_cb(err2) return inner_cb(err2)
end end
@ -320,7 +320,7 @@ M.recursive_copy = function(entry_type, src_path, dest_path, cb)
local waiting = #entries local waiting = #entries
local complete local complete
complete = function(err2) complete = function(err2)
if err then if err2 then
complete = function() end complete = function() end
return inner_cb(err2) return inner_cb(err2)
end end

View file

@ -30,8 +30,6 @@ local M = {}
---@field filter_action? fun(action: oil.Action): boolean When present, filter out actions as they are created ---@field filter_action? fun(action: oil.Action): boolean When present, filter out actions as they are created
---@field filter_error? fun(action: oil.ParseError): boolean When present, filter out errors from parsing a buffer ---@field filter_error? fun(action: oil.ParseError): boolean When present, filter out errors from parsing a buffer
local load_oil_buffer
---Get the entry on a specific line (1-indexed) ---Get the entry on a specific line (1-indexed)
---@param bufnr integer ---@param bufnr integer
---@param lnum integer ---@param lnum integer
@ -224,7 +222,7 @@ M.get_buffer_parent_url = function(bufname, use_oil_parent)
if not use_oil_parent then if not use_oil_parent then
return bufname return bufname
end end
local adapter = config.get_adapter_by_scheme(scheme) local adapter = assert(config.get_adapter_by_scheme(scheme))
local parent_url local parent_url
if adapter and adapter.get_parent then if adapter and adapter.get_parent then
local adapter_scheme = config.adapter_to_scheme[adapter.name] local adapter_scheme = config.adapter_to_scheme[adapter.name]
@ -344,11 +342,16 @@ end
---Open oil browser in a floating window, or close it if open ---Open oil browser in a floating window, or close it if open
---@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file ---@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 if vim.w.is_oil_win then
M.close() M.close()
if cb then
cb()
end
else else
M.open_float(dir) M.open_float(dir, opts, cb)
end end
end end
@ -545,6 +548,8 @@ M.open_preview = function(opts, callback)
end end
util.get_edit_path(bufnr, entry, function(normalized_url) 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() local is_visual_mode = util.is_visual_mode()
if preview_win then if preview_win then
if is_visual_mode then if is_visual_mode then
@ -593,7 +598,7 @@ M.open_preview = function(opts, callback)
-- If we called open_preview during an autocmd, then the edit command may not trigger the -- 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. -- BufReadCmd to load the buffer. So we need to do it manually.
if util.is_oil_bufnr(filebufnr) then if util.is_oil_bufnr(filebufnr) then
load_oil_buffer(filebufnr) M.load_oil_buffer(filebufnr)
end end
vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 }) vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 })
@ -603,7 +608,10 @@ M.open_preview = function(opts, callback)
end end
vim.w.oil_entry_id = entry.id vim.w.oil_entry_id = entry.id
vim.w.oil_source_win = prev_win 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) hack_set_win(prev_win)
-- Restore the visual selection -- Restore the visual selection
vim.cmd.normal({ args = { "gv" }, bang = true }) vim.cmd.normal({ args = { "gv" }, bang = true })
@ -620,6 +628,7 @@ end
---@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier ---@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier
---@field tab? boolean Open the buffer in a new tab ---@field tab? boolean Open the buffer in a new tab
---@field close? boolean Close the original oil buffer once selection is made ---@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 ---Select the entry under the cursor
---@param opts nil|oil.SelectOpts ---@param opts nil|oil.SelectOpts
@ -754,9 +763,14 @@ M.select = function(opts, callback)
local cmd = "buffer" local cmd = "buffer"
if opts.tab then if opts.tab then
vim.cmd.tabnew({ mods = mods }) vim.cmd.tabnew({ mods = mods })
-- Make sure the new buffer from tabnew gets cleaned up
vim.bo.bufhidden = "wipe"
elseif opts.split then elseif opts.split then
cmd = "sbuffer" cmd = "sbuffer"
end end
if opts.handle_buffer_callback ~= nil then
opts.handle_buffer_callback(filebufnr)
else
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch
local ok, err = pcall(vim.cmd, { local ok, err = pcall(vim.cmd, {
cmd = cmd, cmd = cmd,
@ -767,6 +781,7 @@ M.select = function(opts, callback)
if not ok and err and not err:match("^Vim:E325:") then if not ok and err and not err:match("^Vim:E325:") then
vim.api.nvim_echo({ { err, "Error" } }, true, {}) vim.api.nvim_echo({ { err, "Error" } }, true, {})
end end
end
open_next_entry(cb) open_next_entry(cb)
end) end)
@ -818,6 +833,11 @@ end
---@private ---@private
M._get_highlights = function() M._get_highlights = function()
return { return {
{
name = "OilEmpty",
link = "Comment",
desc = "Empty column values",
},
{ {
name = "OilHidden", name = "OilHidden",
link = "Comment", link = "Comment",
@ -1013,8 +1033,9 @@ local function restore_alt_buf()
end end
end end
---@private
---@param bufnr integer ---@param bufnr integer
load_oil_buffer = function(bufnr) M.load_oil_buffer = function(bufnr)
local config = require("oil.config") local config = require("oil.config")
local keymap_util = require("oil.keymap_util") local keymap_util = require("oil.keymap_util")
local loading = require("oil.loading") local loading = require("oil.loading")
@ -1117,9 +1138,9 @@ M.setup = function(opts)
config.setup(opts) config.setup(opts)
set_colors() set_colors()
vim.api.nvim_create_user_command("Oil", function(args) local callback = function(args)
local util = require("oil.util") local util = require("oil.util")
if args.smods.tab == 1 then if args.smods.tab > 0 then
vim.cmd.tabnew() vim.cmd.tabnew()
end end
local float = false local float = false
@ -1152,11 +1173,13 @@ M.setup = function(opts)
end end
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 if args.smods.vertical then
vim.cmd.vsplit({ mods = { split = args.smods.split } }) vim.cmd.vsplit(cmdargs)
else else
vim.cmd.split({ mods = { split = args.smods.split } }) vim.cmd.split(cmdargs)
end end
end end
@ -1172,7 +1195,12 @@ M.setup = function(opts)
open_opts.preview = {} open_opts.preview = {}
end end
M[method](path, open_opts) M[method](path, open_opts)
end, { desc = "Open oil file browser on a directory", nargs = "*", complete = "dir" }) 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", {}) local aug = vim.api.nvim_create_augroup("Oil", {})
if config.default_file_explorer then if config.default_file_explorer then
@ -1218,7 +1246,7 @@ M.setup = function(opts)
pattern = scheme_pattern, pattern = scheme_pattern,
nested = true, nested = true,
callback = function(params) callback = function(params)
load_oil_buffer(params.buf) M.load_oil_buffer(params.buf)
end, end,
}) })
vim.api.nvim_create_autocmd("BufWriteCmd", { vim.api.nvim_create_autocmd("BufWriteCmd", {
@ -1253,8 +1281,7 @@ M.setup = function(opts)
end) end)
vim.cmd.doautocmd({ args = { "BufWritePost", params.file }, mods = { silent = true } }) vim.cmd.doautocmd({ args = { "BufWritePost", params.file }, mods = { silent = true } })
else else
local adapter = config.get_adapter_by_scheme(bufname) local adapter = assert(config.get_adapter_by_scheme(bufname))
assert(adapter)
adapter.write_file(params.buf) adapter.write_file(params.buf)
end end
end, end,
@ -1281,7 +1308,10 @@ M.setup = function(opts)
local util = require("oil.util") local util = require("oil.util")
local bufname = vim.api.nvim_buf_get_name(0) local bufname = vim.api.nvim_buf_get_name(0)
local scheme = util.parse_url(bufname) 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") local view = require("oil.view")
view.maybe_set_cursor() view.maybe_set_cursor()
-- While we are in an oil buffer, set the alternate file to the buffer we were in prior to -- While we are in an oil buffer, set the alternate file to the buffer we were in prior to
@ -1389,7 +1419,7 @@ M.setup = function(opts)
local util = require("oil.util") local util = require("oil.util")
local scheme = util.parse_url(params.file) local scheme = util.parse_url(params.file)
if config.adapters[scheme] and vim.api.nvim_buf_line_count(params.buf) == 1 then 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
end, end,
}) })
@ -1398,7 +1428,7 @@ M.setup = function(opts)
if maybe_hijack_directory_buffer(bufnr) and vim.v.vim_did_enter == 1 then 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 -- manually call load on a hijacked directory buffer if vim has already entered
-- (the BufReadCmd will not trigger) -- (the BufReadCmd will not trigger)
load_oil_buffer(bufnr) M.load_oil_buffer(bufnr)
end end
end end

View file

@ -108,7 +108,7 @@ M.show_help = function(keymaps)
local highlights = {} local highlights = {}
local max_line = 1 local max_line = 1
for _, entry in ipairs(keymap_entries) do for _, entry in ipairs(keymap_entries) do
local line = string.format(" %s %s", util.rpad(entry.str, max_lhs), entry.desc) 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)) max_line = math.max(max_line, vim.api.nvim_strwidth(line))
table.insert(lines, line) table.insert(lines, line)
local start = 1 local start = 1

View file

@ -74,7 +74,8 @@ M.set_loading = function(bufnr, is_loading)
M.set_loading(bufnr, false) M.set_loading(bufnr, false)
return return
end end
local lines = { util.lpad("Loading", math.floor(width / 2) - 3), bar_iter() } local lines =
{ util.pad_align("Loading", math.floor(width / 2) - 3, "right"), bar_iter() }
util.render_text(bufnr, lines) util.render_text(bufnr, lines)
end) end)
) )

View file

@ -68,24 +68,34 @@ local function get_matching_paths(client, filters, paths)
end end
-- Some language servers use forward slashes as path separators on Windows (LuaLS) -- 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("/", "\\") glob = glob:gsub("/", "\\")
end end
---@type string|vim.lpeg.Pattern ---@type string|vim.lpeg.Pattern
local glob_to_match = glob local glob_to_match = glob
if vim.glob and vim.glob.to_lpeg then if vim.glob and vim.glob.to_lpeg then
glob = glob:gsub("{(.-)}", function(s)
local patterns = vim.split(s, ",")
local filtered = {}
for _, pat in ipairs(patterns) do
if pat ~= "" then
table.insert(filtered, pat)
end
end
if #filtered == 0 then
return ""
end
-- HACK around https://github.com/neovim/neovim/issues/28931 -- HACK around https://github.com/neovim/neovim/issues/28931
-- find alternations and sort them by length to try to match the longest first -- find alternations and sort them by length to try to match the longest first
if vim.fn.has("nvim-0.11") == 0 then if vim.fn.has("nvim-0.11") == 0 then
glob = glob:gsub("{(.*)}", function(s) table.sort(filtered, function(a, b)
local pieces = vim.split(s, ",")
table.sort(pieces, function(a, b)
return a:len() > b:len() return a:len() > b:len()
end) end)
return "{" .. table.concat(pieces, ",") .. "}"
end)
end end
return "{" .. table.concat(filtered, ",") .. "}"
end)
glob_to_match = vim.glob.to_lpeg(glob) glob_to_match = vim.glob.to_lpeg(glob)
end end
@ -167,8 +177,13 @@ local function will_file_operation(method, capability_name, files, options)
} }
end, matching_files), end, matching_files),
} }
---@diagnostic disable-next-line: invisible local result, err
local result, err = client.request_sync(method, params, options.timeout_ms or 1000, 0) 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 result and result.result then
if options.apply_edits ~= false then if options.apply_edits ~= false then
vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding) vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding)
@ -204,10 +219,14 @@ local function did_file_operation(method, capability_name, files)
} }
end, matching_files), end, matching_files),
} }
---@diagnostic disable-next-line: invisible 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) client.notify(method, params)
end end
end end
end
end end
--- Notify the server that the client is about to create files. --- Notify the server that the client is about to create files.
@ -280,9 +299,15 @@ function M.will_rename_files(files, options)
} }
end, matching_files), end, matching_files),
} }
local result, err = local result, err
---@diagnostic disable-next-line: invisible 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) client.request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0)
end
if result and result.result then if result and result.result then
if options.apply_edits ~= false then if options.apply_edits ~= false then
vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding) vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding)
@ -313,10 +338,14 @@ function M.did_rename_files(files)
} }
end, matching_files), end, matching_files),
} }
---@diagnostic disable-next-line: invisible 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) client.notify(ms.workspace_didRenameFiles, params)
end end
end end
end
end end
return M return M

View file

@ -85,7 +85,7 @@ M.create_actions_from_diffs = function(all_diffs)
end end
end end
for bufnr, diffs in pairs(all_diffs) do for bufnr, diffs in pairs(all_diffs) do
local adapter = util.get_adapter(bufnr) local adapter = util.get_adapter(bufnr, true)
if not adapter then if not adapter then
error("Missing adapter") error("Missing adapter")
end end
@ -519,7 +519,7 @@ M.try_write_changes = function(confirm, cb)
if vim.bo[bufnr].modified then if vim.bo[bufnr].modified then
local diffs, errors = parser.parse(bufnr) local diffs, errors = parser.parse(bufnr)
all_diffs[bufnr] = diffs all_diffs[bufnr] = diffs
local adapter = assert(util.get_adapter(bufnr)) local adapter = assert(util.get_adapter(bufnr, true))
if adapter.filter_error then if adapter.filter_error then
errors = vim.tbl_filter(adapter.filter_error, errors) errors = vim.tbl_filter(adapter.filter_error, errors)
end end

View file

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

View file

@ -25,46 +25,63 @@ M.escape_filename = function(filename)
return ret return ret
end end
local _url_escape_chars = { local _url_escape_to_char = {
[" "] = "%20", ["20"] = " ",
["$"] = "%24", ["22"] = "",
["&"] = "%26", ["23"] = "#",
["`"] = "%60", ["24"] = "$",
[":"] = "%3A", ["25"] = "%",
["<"] = "%3C", ["26"] = "&",
["="] = "%3D", ["27"] = "",
[">"] = "%3E", ["2B"] = "+",
["?"] = "%3F", ["2C"] = ",",
["["] = "%5B", ["2F"] = "/",
["\\"] = "%5C", ["3A"] = ":",
["]"] = "%5D", ["3B"] = ";",
["^"] = "%5E", ["3C"] = "<",
["{"] = "%7B", ["3D"] = "=",
["|"] = "%7C", ["3E"] = ">",
["}"] = "%7D", ["3F"] = "?",
["~"] = "%7E", ["40"] = "@",
[""] = "%22", ["5B"] = "[",
[""] = "%27", ["5C"] = "\\",
["+"] = "%2B", ["5D"] = "]",
[","] = "%2C", ["5E"] = "^",
["#"] = "%23", ["60"] = "`",
["%"] = "%25", ["7B"] = "{",
["@"] = "%40", ["7C"] = "|",
["/"] = "%2F", ["7D"] = "}",
[";"] = "%3B", ["7E"] = "~",
} }
local _char_to_url_escape = {}
for k, v in pairs(_url_escape_to_char) do
_char_to_url_escape[v] = "%" .. k
end
-- TODO this uri escape handling is very incomplete
---@param string string ---@param string string
---@return string ---@return string
M.url_escape = function(string) M.url_escape = function(string)
return (string:gsub(".", _url_escape_chars)) return (string:gsub(".", _char_to_url_escape))
end
---@param string string
---@return string
M.url_unescape = function(string)
return (
string:gsub("%%([0-9A-Fa-f][0-9A-Fa-f])", function(seq)
return _url_escape_to_char[seq:upper()] or ("%" .. seq)
end)
)
end end
---@param bufnr integer ---@param bufnr integer
---@param silent? boolean
---@return nil|oil.Adapter ---@return nil|oil.Adapter
M.get_adapter = function(bufnr) M.get_adapter = function(bufnr, silent)
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local adapter = config.get_adapter_by_scheme(bufname) local adapter = config.get_adapter_by_scheme(bufname)
if not adapter then if not adapter and not silent then
vim.notify_once( vim.notify_once(
string.format("[oil] could not find adapter for buffer '%s://'", bufname), string.format("[oil] could not find adapter for buffer '%s://'", bufname),
vim.log.levels.ERROR vim.log.levels.ERROR
@ -74,34 +91,28 @@ M.get_adapter = function(bufnr)
end end
---@param text string ---@param text string
---@param length nil|integer ---@param width integer|nil
---@return string ---@param align oil.ColumnAlign
M.rpad = function(text, length) ---@return string padded_text
if not length then ---@return integer left_padding
return text M.pad_align = function(text, width, align)
if not width then
return text, 0
end end
local textlen = vim.api.nvim_strwidth(text) local text_width = vim.api.nvim_strwidth(text)
local delta = length - textlen local total_pad = width - text_width
if delta > 0 then if total_pad <= 0 then
return text .. string.rep(" ", delta) return text, 0
else
return text
end end
end
---@param text string if align == "right" then
---@param length nil|integer return string.rep(" ", total_pad) .. text, total_pad
---@return string elseif align == "center" then
M.lpad = function(text, length) local left_pad = math.floor(total_pad / 2)
if not length then local right_pad = total_pad - left_pad
return text return string.rep(" ", left_pad) .. text .. string.rep(" ", right_pad), left_pad
end
local textlen = vim.api.nvim_strwidth(text)
local delta = length - textlen
if delta > 0 then
return string.rep(" ", delta) .. text
else else
return text return text .. string.rep(" ", total_pad), 0
end end
end end
@ -157,8 +168,10 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
-- This will fail if the dest buf name already exists -- This will fail if the dest buf name already exists
local ok = pcall(vim.api.nvim_buf_set_name, src_bufnr, dest_buf_name) local ok = pcall(vim.api.nvim_buf_set_name, src_bufnr, dest_buf_name)
if ok then if ok then
-- Renaming the buffer creates a new buffer with the old name. Find it and delete it. -- Renaming the buffer creates a new buffer with the old name.
vim.api.nvim_buf_delete(vim.fn.bufadd(bufname), {}) -- Find it and try to delete it, but don't if the buffer is in a context
-- where Neovim doesn't allow buffer modifications.
pcall(vim.api.nvim_buf_delete, vim.fn.bufadd(bufname), {})
if altbuf and vim.api.nvim_buf_is_valid(altbuf) then if altbuf and vim.api.nvim_buf_is_valid(altbuf) then
vim.fn.setreg("#", altbuf) vim.fn.setreg("#", altbuf)
end end
@ -295,11 +308,15 @@ M.split_config = function(name_or_config)
end end
end end
---@alias oil.ColumnAlign "left"|"center"|"right"
---@param lines oil.TextChunk[][] ---@param lines oil.TextChunk[][]
---@param col_width integer[] ---@param col_width integer[]
---@param col_align? oil.ColumnAlign[]
---@return string[] ---@return string[]
---@return any[][] List of highlights {group, lnum, col_start, col_end} ---@return any[][] List of highlights {group, lnum, col_start, col_end}
M.render_table = function(lines, col_width) M.render_table = function(lines, col_width, col_align)
col_align = col_align or {}
local str_lines = {} local str_lines = {}
local highlights = {} local highlights = {}
for _, cols in ipairs(lines) do for _, cols in ipairs(lines) do
@ -313,9 +330,12 @@ M.render_table = function(lines, col_width)
else else
text = chunk text = chunk
end end
text = M.rpad(text, col_width[i])
local unpadded_len = text:len()
local padding
text, padding = M.pad_align(text, col_width[i], col_align[i] or "left")
table.insert(pieces, text) table.insert(pieces, text)
local col_end = col + text:len() + 1
if hl then if hl then
if type(hl) == "table" then if type(hl) == "table" then
-- hl has the form { [1]: hl_name, [2]: col_start, [3]: col_end }[] -- hl has the form { [1]: hl_name, [2]: col_start, [3]: col_end }[]
@ -325,15 +345,15 @@ M.render_table = function(lines, col_width)
table.insert(highlights, { table.insert(highlights, {
sub_hl[1], sub_hl[1],
#str_lines, #str_lines,
col + sub_hl[2], col + padding + sub_hl[2],
col + sub_hl[3], col + padding + sub_hl[3],
}) })
end end
else else
table.insert(highlights, { hl, #str_lines, col, col_end }) table.insert(highlights, { hl, #str_lines, col + padding, col + padding + unpadded_len })
end end
end end
col = col_end col = col + text:len() + 1
end end
table.insert(str_lines, table.concat(pieces, " ")) table.insert(str_lines, table.concat(pieces, " "))
end end
@ -346,7 +366,12 @@ M.set_highlights = function(bufnr, highlights)
local ns = vim.api.nvim_create_namespace("Oil") local ns = vim.api.nvim_create_namespace("Oil")
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
for _, hl in ipairs(highlights) do for _, hl in ipairs(highlights) do
vim.api.nvim_buf_add_highlight(bufnr, ns, unpack(hl)) local group, line, col_start, col_end = unpack(hl)
vim.api.nvim_buf_set_extmark(bufnr, ns, line, col_start, {
end_col = col_end,
hl_group = group,
strict = false,
})
end end
end end
@ -500,10 +525,7 @@ end
---@return oil.Adapter ---@return oil.Adapter
---@return nil|oil.CrossAdapterAction ---@return nil|oil.CrossAdapterAction
M.get_adapter_for_action = function(action) M.get_adapter_for_action = function(action)
local adapter = config.get_adapter_by_scheme(action.url or action.src_url) local adapter = assert(config.get_adapter_by_scheme(action.url or action.src_url))
if not adapter then
error("no adapter found")
end
if action.dest_url then if action.dest_url then
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if adapter ~= dest_adapter then if adapter ~= dest_adapter then
@ -624,11 +646,7 @@ M.render_text = function(bufnr, text, opts)
pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines) pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines)
vim.bo[bufnr].modifiable = false vim.bo[bufnr].modifiable = false
vim.bo[bufnr].modified = false vim.bo[bufnr].modified = false
local ns = vim.api.nvim_create_namespace("Oil") M.set_highlights(bufnr, highlights)
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
for _, hl in ipairs(highlights) do
vim.api.nvim_buf_add_highlight(bufnr, ns, unpack(hl))
end
end end
---Run a function in the context of a full-editor window ---Run a function in the context of a full-editor window
@ -656,8 +674,12 @@ end
---@param bufnr integer ---@param bufnr integer
---@return boolean ---@return boolean
M.is_oil_bufnr = function(bufnr) M.is_oil_bufnr = function(bufnr)
if vim.bo[bufnr].filetype == "oil" then local filetype = vim.bo[bufnr].filetype
if filetype == "oil" then
return true return true
elseif filetype ~= "" then
-- If the filetype is set and is NOT "oil", then it's not an oil buffer
return false
end end
local scheme = M.parse_url(vim.api.nvim_buf_get_name(bufnr)) local scheme = M.parse_url(vim.api.nvim_buf_get_name(bufnr))
return config.adapters[scheme] or config.adapter_aliases[scheme] return config.adapters[scheme] or config.adapter_aliases[scheme]
@ -802,11 +824,12 @@ M.send_to_quickfix = function(opts)
local action = opts.action == "a" and "a" or "r" local action = opts.action == "a" and "a" or "r"
if opts.target == "loclist" then if opts.target == "loclist" then
vim.fn.setloclist(0, {}, action, { title = qf_title, items = qf_entries }) vim.fn.setloclist(0, {}, action, { title = qf_title, items = qf_entries })
vim.cmd.lopen()
else else
vim.fn.setqflist({}, action, { title = qf_title, items = qf_entries }) vim.fn.setqflist({}, action, { title = qf_title, items = qf_entries })
vim.cmd.copen()
end end
vim.api.nvim_exec_autocmds("QuickFixCmdPost", {}) vim.api.nvim_exec_autocmds("QuickFixCmdPost", {})
vim.cmd.copen()
end end
---@return boolean ---@return boolean
@ -887,7 +910,7 @@ M.get_edit_path = function(bufnr, entry, callback)
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local scheme, dir = M.parse_url(bufname) 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) assert(scheme and dir and adapter)
local url = scheme .. dir .. entry.name local url = scheme .. dir .. entry.name
@ -944,8 +967,12 @@ M.read_file_to_scratch_buffer = function(path, preview_method)
vim.bo[bufnr].bufhidden = "wipe" vim.bo[bufnr].bufhidden = "wipe"
vim.bo[bufnr].buftype = "nofile" vim.bo[bufnr].buftype = "nofile"
local max_lines = preview_method == "fast_scratch" and vim.o.lines or nil local has_lines, read_res
local has_lines, read_res = pcall(vim.fn.readfile, path, "", max_lines) 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 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) local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines)

View file

@ -146,7 +146,7 @@ M.unlock_buffers = function()
buffers_locked = false buffers_locked = false
for bufnr in pairs(session) do for bufnr in pairs(session) do
if vim.api.nvim_buf_is_loaded(bufnr) then if vim.api.nvim_buf_is_loaded(bufnr) then
local adapter = util.get_adapter(bufnr) local adapter = util.get_adapter(bufnr, true)
if adapter then if adapter then
vim.bo[bufnr].modifiable = adapter.is_modifiable(bufnr) vim.bo[bufnr].modifiable = adapter.is_modifiable(bufnr)
end end
@ -257,21 +257,14 @@ local function get_first_mutable_column_col(adapter, ranges)
return min_col return min_col
end end
---Force cursor to be after hidden/immutable columns --- @param bufnr integer
---@param mode false|"name"|"editable" --- @param adapter oil.Adapter
local function constrain_cursor(mode) --- @param mode false|"name"|"editable"
if not mode then --- @param cur integer[]
return --- @return integer[] | nil
end local function calc_constrained_cursor_pos(bufnr, adapter, mode, cur)
local parser = require("oil.mutator.parser") local parser = require("oil.mutator.parser")
local line = vim.api.nvim_buf_get_lines(bufnr, cur[1] - 1, cur[1], true)[1]
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 column_defs = columns.get_supported_columns(adapter) local column_defs = columns.get_supported_columns(adapter)
local result = parser.parse_line(adapter, line, column_defs) local result = parser.parse_line(adapter, line, column_defs)
if result and result.ranges then if result and result.ranges then
@ -284,7 +277,45 @@ local function constrain_cursor(mode)
error(string.format('Unexpected value "%s" for option constrain_cursor', mode)) error(string.format('Unexpected value "%s" for option constrain_cursor', mode))
end end
if cur[2] < min_col then 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 end
end end
@ -296,7 +327,7 @@ local function redraw_trash_virtual_text(bufnr)
return return
end end
local parser = require("oil.mutator.parser") 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 if not adapter or adapter.name ~= "trash" then
return return
end end
@ -406,7 +437,7 @@ M.initialize = function(bufnr)
callback = function() callback = function()
-- For some reason the cursor bounces back to its original position, -- For some reason the cursor bounces back to its original position,
-- so we have to defer the call -- so we have to defer the call
vim.schedule_wrap(constrain_cursor)(config.constrain_cursor) vim.schedule_wrap(constrain_cursor)(bufnr, config.constrain_cursor)
end, end,
}) })
vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, { vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, {
@ -419,7 +450,7 @@ M.initialize = function(bufnr)
return return
end end
constrain_cursor(config.constrain_cursor) constrain_cursor(bufnr, config.constrain_cursor)
if config.preview_win.update_on_cursor_moved then if config.preview_win.update_on_cursor_moved then
-- Debounce and update the preview window -- Debounce and update the preview window
@ -456,7 +487,7 @@ M.initialize = function(bufnr)
end, end,
}) })
local adapter = util.get_adapter(bufnr) local adapter = util.get_adapter(bufnr, true)
-- Set up a watcher that will refresh the directory -- Set up a watcher that will refresh the directory
if if
@ -583,7 +614,7 @@ local function get_sort_function(adapter, num_entries)
end end
return function(a, b) return function(a, b)
for _, sort_fn in ipairs(idx_funs) do for _, sort_fn in ipairs(idx_funs) do
local get_sort_value, order = unpack(sort_fn) local get_sort_value, order = sort_fn[1], sort_fn[2]
local a_val = get_sort_value(a) local a_val = get_sort_value(a)
local b_val = get_sort_value(b) local b_val = get_sort_value(b)
if a_val ~= b_val then if a_val ~= b_val then
@ -616,7 +647,7 @@ local function render_buffer(bufnr, opts)
jump_first = false, jump_first = false,
}) })
local scheme = util.parse_url(bufname) local scheme = util.parse_url(bufname)
local adapter = util.get_adapter(bufnr) local adapter = util.get_adapter(bufnr, true)
if not scheme or not adapter then if not scheme or not adapter then
return false return false
end end
@ -637,8 +668,11 @@ local function render_buffer(bufnr, opts)
local column_defs = columns.get_supported_columns(scheme) local column_defs = columns.get_supported_columns(scheme)
local line_table = {} local line_table = {}
local col_width = {} local col_width = {}
for i in ipairs(column_defs) do local col_align = {}
for i, col_def in ipairs(column_defs) do
col_width[i + 1] = 1 col_width[i + 1] = 1
local _, conf = util.split_config(col_def)
col_align[i + 1] = conf and conf.align or "left"
end end
if M.should_display("..", bufnr) then if M.should_display("..", bufnr) then
@ -661,7 +695,7 @@ local function render_buffer(bufnr, opts)
end 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.bo[bufnr].modifiable = true
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
@ -690,7 +724,7 @@ local function render_buffer(bufnr, opts)
end end
end end
constrain_cursor("name") constrain_cursor(bufnr, "name")
end end
end end
end) end)
@ -877,7 +911,7 @@ M.render_buffer_async = function(bufnr, opts, callback)
handle_error(string.format("Could not parse oil url '%s'", bufname)) handle_error(string.format("Could not parse oil url '%s'", bufname))
return return
end end
local adapter = util.get_adapter(bufnr) local adapter = util.get_adapter(bufnr, true)
if not adapter then if not adapter then
handle_error(string.format("[oil] no adapter for buffer '%s'", bufname)) handle_error(string.format("[oil] no adapter for buffer '%s'", bufname))
return return

View file

@ -110,11 +110,16 @@ class ColumnDef:
params: List["LuaParam"] = field(default_factory=list) params: List["LuaParam"] = field(default_factory=list)
HL = [ UNIVERSAL = [
LuaParam( LuaParam(
"highlight", "highlight",
"string|fun(value: string): string", "string|fun(value: string): string",
"Highlight group, or function that returns a highlight group", "Highlight group, or function that returns a highlight group",
),
LuaParam(
"align",
'"left"|"center"|"right"',
"Text alignment within the column",
) )
] ]
TIME = [ TIME = [
@ -127,7 +132,7 @@ COL_DEFS = [
False, False,
True, True,
"The type of the entry (file, directory, link, etc)", "The type of the entry (file, directory, link, etc)",
HL UNIVERSAL
+ [LuaParam("icons", "table<string, string>", "Mapping of entry type to icon")], + [LuaParam("icons", "table<string, string>", "Mapping of entry type to icon")],
), ),
ColumnDef( ColumnDef(
@ -136,7 +141,7 @@ COL_DEFS = [
False, False,
False, False,
"An icon for the entry's type (requires nvim-web-devicons)", "An icon for the entry's type (requires nvim-web-devicons)",
HL UNIVERSAL
+ [ + [
LuaParam( LuaParam(
"default_file", "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( ColumnDef(
"permissions", "permissions",
"files, ssh", "files, ssh",
True, True,
False, False,
"Access permissions of the file", "Access permissions of the file",
HL + [], UNIVERSAL + [],
), ),
ColumnDef( ColumnDef(
"ctime", "files", False, True, "Change timestamp of the file", HL + TIME + [] "ctime", "files", False, True, "Change timestamp of the file", UNIVERSAL + TIME + []
), ),
ColumnDef( 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( 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( ColumnDef(
"birthtime", "birthtime",
"files", "files, s3",
False, False,
True, True,
"The time the file was created", "The time the file was created",
HL + TIME + [], UNIVERSAL + TIME + [],
), ),
] ]

View file

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

View file

@ -168,5 +168,6 @@ a.describe("files adapter", function()
test_util.wait_for_autocmd("BufReadPost") test_util.wait_for_autocmd("BufReadPost")
assert.equals("ruby", vim.bo.filetype) assert.equals("ruby", vim.bo.filetype)
assert.equals(vim.fn.fnamemodify(tmpdir.path, ":p") .. "file.rb", vim.api.nvim_buf_get_name(0)) assert.equals(vim.fn.fnamemodify(tmpdir.path, ":p") .. "file.rb", vim.api.nvim_buf_get_name(0))
assert.equals(tmpdir.path .. "/file.rb", vim.fn.bufname())
end) end)
end) end)

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)