Compare commits

...
Sign in to create a new pull request.

7 commits

Author SHA1 Message Date
8d5be7fc5a
docs(test): add busted migration log
Problem: The plenary-to-busted migration surfaced three test isolation
issues whose root causes and fixes were not documented.

Solution: Add spec/TESTING.md recording the issues, their root causes,
and the applied fixes for future reference. Update CLAUDE.md test
section with correct framework, run command, and test count.
2026-02-22 12:13:58 -05:00
fc15d14bdb
fix(test): resolve busted migration test isolation issues
Problem: Two issues introduced during the plenary-to-busted migration
(6be0148). First, altbuf_spec waited for two BufEnter events but
oil:// → file resolution only fires one async BufEnter (the synchronous
one from vim.cmd.edit fires before wait_for_autocmd is registered).
Second, reset_editor kept the first window which could be a preview
window with stale oil_preview/oil_source_win state, causing
close_preview_window_if_not_in_oil to close the wrong window in
subsequent tests.

Solution: Wait for a single BufEnter in the altbuf test. Replace the
window-keeping logic in reset_editor with vim.cmd.new() +
vim.cmd.only() to guarantee a fresh window with no inherited state.
This also fixes the preview_spec.lua:30 timeout which had the same
root cause. 114/114 tests pass.
2026-02-22 12:13:43 -05:00
36cc369de3
fix(ci): set ft plugin indent 2026-02-22 00:32:41 -05:00
6be0148eef
build: migrate test framework from plenary to busted
Problem: plenary.nvim is deprecated. The test suite depends on
plenary's async test runner and coroutine-based utilities, tying the
project to an unmaintained dependency. CI also tests against Neovim
0.8-0.11, which are no longer relevant.

Solution: replace plenary with busted + nlua (nvim -l). Convert all
async test patterns (a.wrap, a.util.sleep, a.util.scheduler) to
synchronous equivalents using vim.wait. Rename tests/ to spec/ to
follow busted convention. Replace the CI test matrix with
nvim-busted-action targeting stable/nightly only. Add .busted config,
luarocks test_dependencies, and update the nix devshell.
2026-02-22 00:26:54 -05:00
Barrett Ruth
a4da206b67
doc: canonicalize all upstream issues (#21) 2026-02-22 00:04:18 -05:00
Barrett Ruth
86f553cd0a
build: replace luacheck with selene, add nix devshell and pre-commit (#20)
* build: replace luacheck with selene

Problem: luacheck is unmaintained (last release 2018) and required
suppressing four warning classes to avoid false positives. It also
lacks first-class vim/neovim awareness.

Solution: switch to selene with std='vim' for vim-aware linting.
Replace the luacheck CI job with selene, update the Makefile lint
target, and delete .luacheckrc.

* build: add nix devshell and pre-commit hooks

Problem: oil.nvim had no reproducible dev environment. The .envrc
set up a Python venv for the now-removed docgen pipeline, and there
were no pre-commit hooks for local formatting checks.

Solution: add flake.nix with stylua, selene, and prettier in the
devshell. Replace the stale Python .envrc with 'use flake'. Add
.pre-commit-config.yaml with stylua and prettier hooks matching
other plugins in the repo collection.

* fix: format with stylua

* build(selene): configure lints and add inline suppressions

Problem: selene fails on 5 errors and 3 warnings from upstream code
patterns that are intentional (mixed tables in config API, unused
callback parameters, identical if branches for readability).

Solution: globally allow mixed_table and unused_variable (high volume,
inherent to the codebase design). Add inline selene:allow directives
for the 8 remaining issues: if_same_then_else (4), mismatched_arg_count
(1), empty_if (2), global_usage (1). Remove .envrc from tracking.

* build: switch typecheck action to mrcjkb/lua-typecheck-action

Problem: oil.nvim used stevearc/nvim-typecheck-action, which required
cloning the action repo locally for the Makefile lint target. All
other plugins in the collection use mrcjkb/lua-typecheck-action.

Solution: swap to mrcjkb/lua-typecheck-action@v0 for consistency.
Remove the nvim-typecheck-action git clone from the Makefile and
.gitignore. Drop LuaLS from the local lint target since it requires
a full language server install — CI handles it.
2026-02-21 23:52:27 -05:00
Barrett Ruth
df53b172a9
build: add luarocks packaging and bump stylua (#19)
Problem: oil.nvim had no luarocks rockspec, so users of rocks.nvim
and similar tools could not install it from the registry. The stylua
CI action was also pinned to an older version.

Solution: add scm-1 rockspec and a luarocks publish workflow that
gates on tests passing before publishing on version tags. Bump
stylua action from v2.0.2 to v2.1.0.

Closes: barrettruth/oil.nvim#14
2026-02-21 23:28:22 -05:00
90 changed files with 3911 additions and 3649 deletions

9
.busted Normal file
View file

@ -0,0 +1,9 @@
return {
_all = {
lua = "nlua",
},
default = {
ROOT = { "./spec/" },
helper = "spec/minimal_init.lua",
},
}

3
.envrc
View file

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

View file

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

21
.github/workflows/luarocks.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: luarocks
on:
push:
tags:
- 'v*'
jobs:
tests:
uses: ./.github/workflows/tests.yml
publish:
needs: tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: nvim-neorocks/luarocks-tag-release@v7
env:
LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}

View file

@ -10,63 +10,51 @@ on:
- main - main
jobs: jobs:
luacheck: selene:
name: Luacheck name: Selene
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: NTBBloodbath/selene-action@v1.0.0
- name: Prepare with:
run: | token: ${{ secrets.GITHUB_TOKEN }}
sudo apt-get update args: --display-style quiet .
sudo add-apt-repository universe
sudo apt install luarocks -y
sudo luarocks install luacheck
- name: Run Luacheck
run: luacheck lua tests
stylua: stylua:
name: StyLua name: StyLua
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Stylua - name: Stylua
uses: JohnnyMorganz/stylua-action@v4 uses: JohnnyMorganz/stylua-action@v4
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
version: v2.0.2 version: v2.1.0
args: --check lua tests args: --check lua spec
typecheck: typecheck:
name: typecheck name: typecheck
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: stevearc/nvim-typecheck-action@v2 - uses: mrcjkb/lua-typecheck-action@v0
with: with:
path: lua checklevel: Warning
directories: lua
configpath: .luarc.json
run_tests: run_tests:
strategy: strategy:
fail-fast: false
matrix: matrix:
include: nvim_version:
- nvim_tag: v0.8.3 - stable
- nvim_tag: v0.9.4 - nightly
- nvim_tag: v0.10.4
- nvim_tag: v0.11.0
name: Run tests name: Run tests
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
env:
NVIM_TAG: ${{ matrix.nvim_tag }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: nvim-neorocks/nvim-busted-action@v1
- name: Install Neovim and dependencies with:
run: | nvim_version: ${{ matrix.nvim_version }}
bash ./.github/workflows/install_nvim.sh
- name: Run tests
run: |
bash ./run_tests.sh

3
.gitignore vendored
View file

@ -40,9 +40,8 @@ luac.out
*.hex *.hex
.direnv/ .direnv/
.testenv/ .envrc
doc/tags doc/tags
scripts/nvim-typecheck-action
scripts/benchmark.nvim scripts/benchmark.nvim
perf/tmp/ perf/tmp/
profile.json profile.json

View file

@ -1,19 +0,0 @@
max_comment_line_length = false
codes = true
exclude_files = {
"tests/treesitter",
}
ignore = {
"212", -- Unused argument
"631", -- Line is too long
"122", -- Setting a readonly global
"542", -- Empty if branch
}
read_globals = {
"vim",
"a",
"assert",
}

View file

@ -1,9 +1,8 @@
{ {
"runtime": { "runtime.version": "Lua 5.1",
"version": "LuaJIT", "runtime.path": ["lua/?.lua", "lua/?/init.lua"],
"pathStrict": true "diagnostics.globals": ["vim", "jit", "bit"],
}, "workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"],
"type": { "workspace.checkThirdParty": false,
"checkTableShape": true "completion.callSnippet": "Replace"
}
} }

17
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,17 @@
minimum_pre_commit_version: '3.5.0'
repos:
- repo: https://github.com/JohnnyMorganz/StyLua
rev: v2.3.1
hooks:
- id: stylua-github
name: stylua (Lua formatter)
files: \.lua$
pass_filenames: true
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
name: prettier
files: \.(md|toml|yaml|yml|sh)$

View file

@ -1,5 +1,8 @@
column_width = 100 column_width = 100
line_endings = "Unix"
indent_type = "Spaces" indent_type = "Spaces"
indent_width = 2 indent_width = 2
quote_style = "AutoPreferSingle"
call_parentheses = "Always"
[sort_requires] [sort_requires]
enabled = true enabled = true

View file

@ -11,14 +11,13 @@ all: lint test
## test: run tests ## test: run tests
.PHONY: test .PHONY: test
test: test:
./run_tests.sh luarocks test --local
## lint: run linters and LuaLS typechecking ## lint: run selene and stylua
.PHONY: lint .PHONY: lint
lint: scripts/nvim-typecheck-action lint:
./scripts/nvim-typecheck-action/typecheck.sh --workdir scripts/nvim-typecheck-action lua selene --display-style quiet .
luacheck lua tests --formatter plain stylua --check lua spec
stylua --check lua tests
## profile: use LuaJIT profiler to profile the plugin ## profile: use LuaJIT profiler to profile the plugin
.PHONY: profile .PHONY: profile
@ -36,13 +35,10 @@ benchmark: scripts/benchmark.nvim
nvim --clean -u perf/bootstrap.lua -c 'lua benchmark()' nvim --clean -u perf/bootstrap.lua -c 'lua benchmark()'
@cat perf/tmp/benchmark.txt @cat perf/tmp/benchmark.txt
scripts/nvim-typecheck-action:
git clone https://github.com/stevearc/nvim-typecheck-action scripts/nvim-typecheck-action
scripts/benchmark.nvim: scripts/benchmark.nvim:
git clone https://github.com/stevearc/benchmark.nvim scripts/benchmark.nvim git clone https://github.com/stevearc/benchmark.nvim scripts/benchmark.nvim
## clean: reset the repository to a clean state ## clean: reset the repository to a clean state
.PHONY: clean .PHONY: clean
clean: clean:
rm -rf scripts/nvim-typecheck-action venv .testenv perf/tmp profile.json rm -rf perf/tmp profile.json

167
README.md
View file

@ -11,51 +11,154 @@ upstream yet.
<details> <details>
<summary>Changes from upstream</summary> <summary>Changes from upstream</summary>
### PRs ### Cherry-picked PRs
Upstream PRs cherry-picked or adapted into this fork. Upstream PRs cherry-picked or adapted into this fork.
| PR | Description | Commit | | PR | Description | Commit |
|---|---|---| |---|---|---|
| [#495](https://github.com/stevearc/oil.nvim/pull/495) | Cancel visual/operator-pending mode on close instead of closing buffer | [`16f3d7b`](https://github.com/barrettruth/oil.nvim/commit/16f3d7b) | | [#495](https://github.com/stevearc/oil.nvim/pull/495) | Cancel visual/operator-pending mode on close | [`16f3d7b`](https://github.com/barrettruth/oil.nvim/commit/16f3d7b) |
| [#537](https://github.com/stevearc/oil.nvim/pull/537) | Configurable file and directory creation permissions (`new_file_mode`, `new_dir_mode`) | [`c6b4a7a`](https://github.com/barrettruth/oil.nvim/commit/c6b4a7a) | | [#537](https://github.com/stevearc/oil.nvim/pull/537) | Configurable file/directory creation permissions | [`c6b4a7a`](https://github.com/barrettruth/oil.nvim/commit/c6b4a7a) |
| [#578](https://github.com/stevearc/oil.nvim/issues/578) | Recipe to disable hidden file dimming by relinking `Oil*Hidden` groups | [`38db6cf`](https://github.com/barrettruth/oil.nvim/commit/38db6cf) | | [#618](https://github.com/stevearc/oil.nvim/pull/618) | Opt-in filetype detection for icons | [`ded1725`](https://github.com/barrettruth/oil.nvim/commit/ded1725) |
| [#618](https://github.com/stevearc/oil.nvim/pull/618) | Opt-in filetype detection for icons via `use_slow_filetype_detection` | [`ded1725`](https://github.com/barrettruth/oil.nvim/commit/ded1725) | | [#644](https://github.com/stevearc/oil.nvim/pull/644) | Pass entry to `is_hidden_file`/`is_always_hidden` | [`4ab4765`](https://github.com/barrettruth/oil.nvim/commit/4ab4765) |
| [#644](https://github.com/stevearc/oil.nvim/pull/644) | Pass full entry to `is_hidden_file` and `is_always_hidden` callbacks | [`4ab4765`](https://github.com/barrettruth/oil.nvim/commit/4ab4765) | | [#697](https://github.com/stevearc/oil.nvim/pull/697) | Recipe for file extension column | [`dcb3a08`](https://github.com/barrettruth/oil.nvim/commit/dcb3a08) |
| [#645](https://github.com/stevearc/oil.nvim/pull/645) | Add `close_float` action (close only floating oil windows) | [`f6bcdda`](https://github.com/barrettruth/oil.nvim/commit/f6bcdda) | | [#698](https://github.com/stevearc/oil.nvim/pull/698) | Executable file highlighting | [`41556ec`](https://github.com/barrettruth/oil.nvim/commit/41556ec), [`85ed9b8`](https://github.com/barrettruth/oil.nvim/commit/85ed9b8) |
| [#690](https://github.com/stevearc/oil.nvim/pull/690) | Add `OilFileIcon` highlight group as fallback for unrecognized icons | [`ce64ae1`](https://github.com/barrettruth/oil.nvim/commit/ce64ae1) | | [#717](https://github.com/stevearc/oil.nvim/pull/717) | Add oil-git.nvim to extensions | [`582d9fc`](https://github.com/barrettruth/oil.nvim/commit/582d9fc) |
| [#697](https://github.com/stevearc/oil.nvim/pull/697) | Recipe for custom file extension column with sorting | [`dcb3a08`](https://github.com/barrettruth/oil.nvim/commit/dcb3a08) | | [#720](https://github.com/stevearc/oil.nvim/pull/720) | Gate `BufAdd` autocmd behind config check | [`2228f80`](https://github.com/barrettruth/oil.nvim/commit/2228f80) |
| [#698](https://github.com/stevearc/oil.nvim/pull/698) | Executable file highlighting (`OilExecutable`, `OilExecutableHidden`) | [`41556ec`](https://github.com/barrettruth/oil.nvim/commit/41556ec), [`85ed9b8`](https://github.com/barrettruth/oil.nvim/commit/85ed9b8) | | [#722](https://github.com/stevearc/oil.nvim/pull/722) | Fix freedesktop trash URL | [`b92ecb0`](https://github.com/barrettruth/oil.nvim/commit/b92ecb0) |
| [#717](https://github.com/stevearc/oil.nvim/pull/717) | Add malewicz1337/oil-git.nvim to third-party extensions | [`582d9fc`](https://github.com/barrettruth/oil.nvim/commit/582d9fc) | | [#723](https://github.com/stevearc/oil.nvim/pull/723) | Emit `OilReadPost` event after render | [`29239d5`](https://github.com/barrettruth/oil.nvim/commit/29239d5) |
| [#720](https://github.com/stevearc/oil.nvim/pull/720) | Gate `BufAdd` autocmd behind `default_file_explorer` check | [`2228f80`](https://github.com/barrettruth/oil.nvim/commit/2228f80) | | [#725](https://github.com/stevearc/oil.nvim/pull/725) | Normalize keymap keys before config merge | [`723145c`](https://github.com/barrettruth/oil.nvim/commit/723145c) |
| [#722](https://github.com/stevearc/oil.nvim/pull/722) | Fix dead freedesktop trash specification URL | [`b92ecb0`](https://github.com/barrettruth/oil.nvim/commit/b92ecb0) | | [#727](https://github.com/stevearc/oil.nvim/pull/727) | Clarify `get_current_dir` nil + Telescope recipe | [`eed6697`](https://github.com/barrettruth/oil.nvim/commit/eed6697) |
| [#723](https://github.com/stevearc/oil.nvim/pull/723) | Emit `OilReadPost` user event after every buffer render | [`29239d5`](https://github.com/barrettruth/oil.nvim/commit/29239d5) |
| [#725](https://github.com/stevearc/oil.nvim/pull/725) | Normalize keymap keys before config merge (`<c-t>` = `<C-t>`) | [`723145c`](https://github.com/barrettruth/oil.nvim/commit/723145c) | ### Open upstream PRs
| [#727](https://github.com/stevearc/oil.nvim/pull/727) | Clarify `get_current_dir` nil return and add Telescope recipe | [`eed6697`](https://github.com/barrettruth/oil.nvim/commit/eed6697) |
Open PRs on upstream not yet incorporated into this fork.
| PR | Description | Status |
|---|---|---|
| [#488](https://github.com/stevearc/oil.nvim/pull/488) | Parent directory in a split | not actionable — empty PR, 0 changes |
| [#493](https://github.com/stevearc/oil.nvim/pull/493) | UNC paths on Windows | not actionable — superseded by [#686](https://github.com/stevearc/oil.nvim/pull/686) |
| [#591](https://github.com/stevearc/oil.nvim/pull/591) | release-please changelog | not applicable — fork uses different release process |
| [#667](https://github.com/stevearc/oil.nvim/pull/667) | Virtual text columns + headers | deferred — WIP, conflicting |
| [#686](https://github.com/stevearc/oil.nvim/pull/686) | Windows path conversion fix | not actionable — Windows-only |
| [#708](https://github.com/stevearc/oil.nvim/pull/708) | Move file into new dir by renaming | deferred — needs rewrite for multi-level dirs, tests |
| [#721](https://github.com/stevearc/oil.nvim/pull/721) | `create_hook` to populate file contents | open |
| [#728](https://github.com/stevearc/oil.nvim/pull/728) | `open_split` for opening oil in a split | tracked — [barrettruth/oil.nvim#2](https://github.com/barrettruth/oil.nvim/issues/2) |
### Issues ### Issues
Upstream issues triaged against this fork. All open upstream issues, triaged against this fork.
| Issue | Status | Resolution | **Status key:** `fixed` = original fix in fork, `resolved` = addressed by cherry-picked PR,
`not actionable` = can't/won't fix, `tracking` = known/not yet addressed, `open` = not yet triaged.
| Issue | Status | Notes |
|---|---|---| |---|---|---|
| [#446](https://github.com/stevearc/oil.nvim/issues/446) | resolved | Executable highlighting — implemented by PR [#698](https://github.com/stevearc/oil.nvim/pull/698) | | [#85](https://github.com/stevearc/oil.nvim/issues/85) | open | Git status column (P2) |
| [#483](https://github.com/stevearc/oil.nvim/issues/483) | not actionable | Spell downloads depend on netrw — fixed in neovim ([neovim#34940](https://github.com/neovim/neovim/pull/34940)) | | [#95](https://github.com/stevearc/oil.nvim/issues/95) | open | Undo after renaming files (P1) |
| [#492](https://github.com/stevearc/oil.nvim/issues/492) | not actionable | Question — j/k remapping, answered in comments | | [#117](https://github.com/stevearc/oil.nvim/issues/117) | open | Move file into new dir via slash in name (P1, related to [#708](https://github.com/stevearc/oil.nvim/pull/708)) |
| [#533](https://github.com/stevearc/oil.nvim/issues/533) | not actionable | `constrain_cursor` — needs repro from reporter | | [#156](https://github.com/stevearc/oil.nvim/issues/156) | open | Paste path of files into oil buffer (P2) |
| [#200](https://github.com/stevearc/oil.nvim/issues/200) | open | Highlights not working when opening a file (P2) |
| [#207](https://github.com/stevearc/oil.nvim/issues/207) | open | Suppress "no longer available" message (P1) |
| [#210](https://github.com/stevearc/oil.nvim/issues/210) | open | FTP support (P2) |
| [#213](https://github.com/stevearc/oil.nvim/issues/213) | open | Disable preview for large files (P1) |
| [#226](https://github.com/stevearc/oil.nvim/issues/226) | open | K8s/Docker adapter (P2) |
| [#232](https://github.com/stevearc/oil.nvim/issues/232) | open | Cannot close last window (P2) |
| [#254](https://github.com/stevearc/oil.nvim/issues/254) | open | Buffer modified highlight group (P2) |
| [#263](https://github.com/stevearc/oil.nvim/issues/263) | open | Diff mode (P2) |
| [#276](https://github.com/stevearc/oil.nvim/issues/276) | open | Archives manipulation (P2) |
| [#280](https://github.com/stevearc/oil.nvim/issues/280) | open | vim-projectionist support (P2) |
| [#288](https://github.com/stevearc/oil.nvim/issues/288) | open | Oil failing to load (P2) |
| [#289](https://github.com/stevearc/oil.nvim/issues/289) | open | Show absolute path toggle (P2) |
| [#294](https://github.com/stevearc/oil.nvim/issues/294) | open | Can't handle emojis in filenames (P2) |
| [#298](https://github.com/stevearc/oil.nvim/issues/298) | open | Open float on neovim directory startup (P2) |
| [#302](https://github.com/stevearc/oil.nvim/issues/302) | open | C-o parent nav makes buffer buflisted (P0) |
| [#303](https://github.com/stevearc/oil.nvim/issues/303) | open | Preview in float window mode (P2) |
| [#325](https://github.com/stevearc/oil.nvim/issues/325) | open | oil-ssh error from command line (P0) |
| [#330](https://github.com/stevearc/oil.nvim/issues/330) | open | File opens in floating modal |
| [#332](https://github.com/stevearc/oil.nvim/issues/332) | open | Buffer not fixed to floating window (P2) |
| [#335](https://github.com/stevearc/oil.nvim/issues/335) | open | Disable editing outside root dir |
| [#349](https://github.com/stevearc/oil.nvim/issues/349) | open | Parent directory as column/vsplit (P2) |
| [#351](https://github.com/stevearc/oil.nvim/issues/351) | open | Paste deleted file from register |
| [#359](https://github.com/stevearc/oil.nvim/issues/359) | open | Parse error on filenames differing by space (P1) |
| [#360](https://github.com/stevearc/oil.nvim/issues/360) | open | Pick window to open file into |
| [#362](https://github.com/stevearc/oil.nvim/issues/362) | open | "Could not find oil adapter for scheme" error |
| [#363](https://github.com/stevearc/oil.nvim/issues/363) | open | `prompt_save_on_select_new_entry` uses wrong prompt |
| [#371](https://github.com/stevearc/oil.nvim/issues/371) | open | Constrain cursor in insert mode |
| [#373](https://github.com/stevearc/oil.nvim/issues/373) | open | Dir from quickfix with bqf/trouble broken (P1) |
| [#375](https://github.com/stevearc/oil.nvim/issues/375) | open | Highlights for file types and permissions (P2) |
| [#380](https://github.com/stevearc/oil.nvim/issues/380) | open | Show file in oil when editing hidden file |
| [#382](https://github.com/stevearc/oil.nvim/issues/382) | open | Relative path in window title (P2) |
| [#392](https://github.com/stevearc/oil.nvim/issues/392) | open | Option to skip delete prompt |
| [#393](https://github.com/stevearc/oil.nvim/issues/393) | open | Auto-save new buffer on entry |
| [#396](https://github.com/stevearc/oil.nvim/issues/396) | open | Customize preview content (P2) |
| [#399](https://github.com/stevearc/oil.nvim/issues/399) | open | Open file without closing Oil (P1) |
| [#404](https://github.com/stevearc/oil.nvim/issues/404) | not actionable | Restricted UNC paths — Windows-only (P2) |
| [#416](https://github.com/stevearc/oil.nvim/issues/416) | open | Cannot remap key to open split |
| [#431](https://github.com/stevearc/oil.nvim/issues/431) | open | More SSH adapter documentation |
| [#435](https://github.com/stevearc/oil.nvim/issues/435) | open | Error previewing with semantic tokens LSP |
| [#436](https://github.com/stevearc/oil.nvim/issues/436) | open | Owner and group columns (P2) |
| [#444](https://github.com/stevearc/oil.nvim/issues/444) | open | Opening behaviour customization |
| [#446](https://github.com/stevearc/oil.nvim/issues/446) | resolved | Executable highlighting — PR [#698](https://github.com/stevearc/oil.nvim/pull/698) |
| [#449](https://github.com/stevearc/oil.nvim/issues/449) | open | Renaming TypeScript files stopped working |
| [#450](https://github.com/stevearc/oil.nvim/issues/450) | open | Highlight opened file in directory listing |
| [#457](https://github.com/stevearc/oil.nvim/issues/457) | open | Custom column API |
| [#466](https://github.com/stevearc/oil.nvim/issues/466) | open | Select into window on right |
| [#473](https://github.com/stevearc/oil.nvim/issues/473) | open | Show all hidden files if dir only has hidden |
| [#479](https://github.com/stevearc/oil.nvim/issues/479) | open | Harpoon integration recipe |
| [#483](https://github.com/stevearc/oil.nvim/issues/483) | not actionable | Spell downloads depend on netrw — fixed in [neovim#34940](https://github.com/neovim/neovim/pull/34940) |
| [#486](https://github.com/stevearc/oil.nvim/issues/486) | open | All directory sizes show 4.1k |
| [#492](https://github.com/stevearc/oil.nvim/issues/492) | not actionable | j/k remapping question — answered in comments |
| [#507](https://github.com/stevearc/oil.nvim/issues/507) | open | lacasitos.nvim conflict (P1) |
| [#521](https://github.com/stevearc/oil.nvim/issues/521) | open | oil-ssh connection issues |
| [#525](https://github.com/stevearc/oil.nvim/issues/525) | open | SSH adapter documentation (P2) |
| [#531](https://github.com/stevearc/oil.nvim/issues/531) | not actionable | Windows — incomplete drive letters (P1) |
| [#533](https://github.com/stevearc/oil.nvim/issues/533) | not actionable | `constrain_cursor` — needs repro |
| [#570](https://github.com/stevearc/oil.nvim/issues/570) | open | Improve c0/d0 for renaming |
| [#571](https://github.com/stevearc/oil.nvim/issues/571) | open | Callback before `highlight_filename` (P2) |
| [#578](https://github.com/stevearc/oil.nvim/issues/578) | resolved | Hidden file dimming recipe — [`38db6cf`](https://github.com/barrettruth/oil.nvim/commit/38db6cf) |
| [#587](https://github.com/stevearc/oil.nvim/issues/587) | not actionable | Alt+h keymap — user config issue | | [#587](https://github.com/stevearc/oil.nvim/issues/587) | not actionable | Alt+h keymap — user config issue |
| [#623](https://github.com/stevearc/oil.nvim/issues/623) | not actionable | bufferline.nvim interaction — cross-plugin issue | | [#599](https://github.com/stevearc/oil.nvim/issues/599) | open | user:group display and manipulation (P2) |
| [#624](https://github.com/stevearc/oil.nvim/issues/624) | not actionable | Mutation-in-progress race — no reliable repro | | [#607](https://github.com/stevearc/oil.nvim/issues/607) | open | Per-host SCP args (P2) |
| [#632](https://github.com/stevearc/oil.nvim/issues/632) | fixed | Preview + move = copy — [`fe16993`](https://github.com/barrettruth/oil.nvim/commit/fe16993) | | [#609](https://github.com/stevearc/oil.nvim/issues/609) | open | Cursor placement via Snacks picker |
| [#612](https://github.com/stevearc/oil.nvim/issues/612) | open | Delete buffers on file delete (P2) |
| [#615](https://github.com/stevearc/oil.nvim/issues/615) | open | Cursor at name column on o/O (P2) |
| [#617](https://github.com/stevearc/oil.nvim/issues/617) | open | Filetype by actual filetype (P2) |
| [#621](https://github.com/stevearc/oil.nvim/issues/621) | open | Toggle function for regular windows (P2) |
| [#623](https://github.com/stevearc/oil.nvim/issues/623) | not actionable | bufferline.nvim interaction — cross-plugin |
| [#624](https://github.com/stevearc/oil.nvim/issues/624) | not actionable | Mutation race — no reliable repro |
| [#625](https://github.com/stevearc/oil.nvim/issues/625) | not actionable | E19 mark invalid line — intractable without neovim API changes |
| [#632](https://github.com/stevearc/oil.nvim/issues/632) | fixed | Preview + move = copy — [PR #12](https://github.com/barrettruth/oil.nvim/pull/12) ([`fe16993`](https://github.com/barrettruth/oil.nvim/commit/fe16993)) |
| [#636](https://github.com/stevearc/oil.nvim/issues/636) | open | Telescope picker opens in active buffer |
| [#637](https://github.com/stevearc/oil.nvim/issues/637) | open | Inconsistent symlink resolution |
| [#641](https://github.com/stevearc/oil.nvim/issues/641) | open | Flicker on `actions.parent` |
| [#642](https://github.com/stevearc/oil.nvim/issues/642) | fixed | W10 warning under `nvim -R` — [`ca834cf`](https://github.com/barrettruth/oil.nvim/commit/ca834cf) | | [#642](https://github.com/stevearc/oil.nvim/issues/642) | fixed | W10 warning under `nvim -R` — [`ca834cf`](https://github.com/barrettruth/oil.nvim/commit/ca834cf) |
| [#664](https://github.com/stevearc/oil.nvim/issues/664) | not actionable | Extra buffer on session reload — no repro | | [#645](https://github.com/stevearc/oil.nvim/issues/645) | resolved | `close_float` action — [`f6bcdda`](https://github.com/barrettruth/oil.nvim/commit/f6bcdda) |
| [#670](https://github.com/stevearc/oil.nvim/issues/670) | fixed | Multi-directory cmdline — [`70861e5`](https://github.com/barrettruth/oil.nvim/commit/70861e5) | | [#646](https://github.com/stevearc/oil.nvim/issues/646) | open | `get_current_dir` nil on SSH |
| [#650](https://github.com/stevearc/oil.nvim/issues/650) | open | Emit LSP `workspace.fileOperations` events |
| [#655](https://github.com/stevearc/oil.nvim/issues/655) | open | File statistics as virtual text |
| [#659](https://github.com/stevearc/oil.nvim/issues/659) | open | Mark and diff files in buffer |
| [#664](https://github.com/stevearc/oil.nvim/issues/664) | not actionable | Session reload extra buffer — no repro |
| [#665](https://github.com/stevearc/oil.nvim/issues/665) | open | Hot load preview fast-scratch buffers |
| [#668](https://github.com/stevearc/oil.nvim/issues/668) | open | Custom yes/no confirmation |
| [#670](https://github.com/stevearc/oil.nvim/issues/670) | fixed | Multi-directory cmdline — [PR #11](https://github.com/barrettruth/oil.nvim/pull/11) ([`70861e5`](https://github.com/barrettruth/oil.nvim/commit/70861e5)) |
| [#671](https://github.com/stevearc/oil.nvim/issues/671) | open | Yanking between nvim instances |
| [#673](https://github.com/stevearc/oil.nvim/issues/673) | fixed | Symlink newlines crash — [`9110a1a`](https://github.com/barrettruth/oil.nvim/commit/9110a1a) | | [#673](https://github.com/stevearc/oil.nvim/issues/673) | fixed | Symlink newlines crash — [`9110a1a`](https://github.com/barrettruth/oil.nvim/commit/9110a1a) |
| [#679](https://github.com/stevearc/oil.nvim/issues/679) | resolved | Executable file sign — implemented by PR [#698](https://github.com/stevearc/oil.nvim/pull/698) | | [#675](https://github.com/stevearc/oil.nvim/issues/675) | open | Move file into folder by renaming (related to [#708](https://github.com/stevearc/oil.nvim/pull/708)) |
| [#692](https://github.com/stevearc/oil.nvim/issues/692) | resolved | Keymap normalization — fixed by PR [#725](https://github.com/stevearc/oil.nvim/pull/725) | | [#676](https://github.com/stevearc/oil.nvim/issues/676) | not actionable | Windows — path conversion |
| [#710](https://github.com/stevearc/oil.nvim/issues/710) | fixed | buftype empty on BufEnter — [`01b860e`](https://github.com/barrettruth/oil.nvim/commit/01b860e) | | [#678](https://github.com/stevearc/oil.nvim/issues/678) | tracking | `buftype='acwrite'` causes `mksession` to skip oil windows |
| [#714](https://github.com/stevearc/oil.nvim/issues/714) | not actionable | Support question — already answered | | [#679](https://github.com/stevearc/oil.nvim/issues/679) | resolved | Executable file sign — PR [#698](https://github.com/stevearc/oil.nvim/pull/698) |
| [#719](https://github.com/stevearc/oil.nvim/issues/719) | not actionable | Neovim crash on node_modules delete — libuv/neovim bug | | [#682](https://github.com/stevearc/oil.nvim/issues/682) | open | `get_current_dir()` nil in non-telescope context |
| [#683](https://github.com/stevearc/oil.nvim/issues/683) | open | Path not shown in floating mode |
| [#684](https://github.com/stevearc/oil.nvim/issues/684) | open | User and group columns |
| [#685](https://github.com/stevearc/oil.nvim/issues/685) | open | Plain directory paths in buffer names |
| [#690](https://github.com/stevearc/oil.nvim/issues/690) | resolved | `OilFileIcon` highlight group — [`ce64ae1`](https://github.com/barrettruth/oil.nvim/commit/ce64ae1) |
| [#692](https://github.com/stevearc/oil.nvim/issues/692) | resolved | Keymap normalization — PR [#725](https://github.com/stevearc/oil.nvim/pull/725) |
| [#699](https://github.com/stevearc/oil.nvim/issues/699) | open | `select` blocks UI with slow FileType autocmd |
| [#707](https://github.com/stevearc/oil.nvim/issues/707) | open | Move file/dir into new dir by renaming (related to [#708](https://github.com/stevearc/oil.nvim/pull/708)) |
| [#710](https://github.com/stevearc/oil.nvim/issues/710) | fixed | buftype empty on BufEnter — [PR #10](https://github.com/barrettruth/oil.nvim/pull/10) ([`01b860e`](https://github.com/barrettruth/oil.nvim/commit/01b860e)) |
| [#714](https://github.com/stevearc/oil.nvim/issues/714) | not actionable | Support question — answered |
| [#719](https://github.com/stevearc/oil.nvim/issues/719) | not actionable | Neovim crash on node_modules — libuv/neovim bug |
| [#726](https://github.com/stevearc/oil.nvim/issues/726) | not actionable | Meta discussion/roadmap | | [#726](https://github.com/stevearc/oil.nvim/issues/726) | not actionable | Meta discussion/roadmap |
</details> </details>

43
flake.lock generated Normal file
View file

@ -0,0 +1,43 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1771207753,
"narHash": "sha256-b9uG8yN50DRQ6A7JdZBfzq718ryYrlmGgqkRm9OOwCE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d1c15b7d5806069da59e819999d70e1cec0760bf",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"systems": "systems"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

33
flake.nix Normal file
View file

@ -0,0 +1,33 @@
{
description = "oil.nvim Neovim file explorer: edit your filesystem like a buffer";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
systems.url = "github:nix-systems/default";
};
outputs =
{
nixpkgs,
systems,
...
}:
let
forEachSystem = f: nixpkgs.lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system});
in
{
devShells = forEachSystem (pkgs: {
default = pkgs.mkShell {
packages = [
pkgs.prettier
pkgs.stylua
pkgs.selene
(pkgs.luajit.withPackages (ps: [
ps.busted
ps.nlua
]))
];
};
});
};
}

View file

@ -1,18 +1,18 @@
local oil = require("oil") local oil = require('oil')
local util = require("oil.util") local util = require('oil.util')
local M = {} local M = {}
M.show_help = { M.show_help = {
callback = function() callback = function()
local config = require("oil.config") local config = require('oil.config')
require("oil.keymap_util").show_help(config.keymaps) require('oil.keymap_util').show_help(config.keymaps)
end, end,
desc = "Show default keymaps", desc = 'Show default keymaps',
} }
M.select = { M.select = {
desc = "Open the entry under the cursor", desc = 'Open the entry under the cursor',
callback = function(opts) callback = function(opts)
opts = opts or {} opts = opts or {}
local callback = opts.callback local callback = opts.callback
@ -21,30 +21,30 @@ M.select = {
end, end,
parameters = { parameters = {
vertical = { vertical = {
type = "boolean", type = 'boolean',
desc = "Open the buffer in a vertical split", desc = 'Open the buffer in a vertical split',
}, },
horizontal = { horizontal = {
type = "boolean", type = 'boolean',
desc = "Open the buffer in a horizontal split", desc = 'Open the buffer in a horizontal split',
}, },
split = { split = {
type = '"aboveleft"|"belowright"|"topleft"|"botright"', type = '"aboveleft"|"belowright"|"topleft"|"botright"',
desc = "Split modifier", desc = 'Split modifier',
}, },
tab = { tab = {
type = "boolean", type = 'boolean',
desc = "Open the buffer in a new tab", desc = 'Open the buffer in a new tab',
}, },
close = { close = {
type = "boolean", type = 'boolean',
desc = "Close the original oil buffer once selection is made", desc = 'Close the original oil buffer once selection is made',
}, },
}, },
} }
M.select_vsplit = { M.select_vsplit = {
desc = "Open the entry under the cursor in a vertical split", desc = 'Open the entry under the cursor in a vertical split',
deprecated = true, deprecated = true,
callback = function() callback = function()
oil.select({ vertical = true }) oil.select({ vertical = true })
@ -52,7 +52,7 @@ M.select_vsplit = {
} }
M.select_split = { M.select_split = {
desc = "Open the entry under the cursor in a horizontal split", desc = 'Open the entry under the cursor in a horizontal split',
deprecated = true, deprecated = true,
callback = function() callback = function()
oil.select({ horizontal = true }) oil.select({ horizontal = true })
@ -60,7 +60,7 @@ M.select_split = {
} }
M.select_tab = { M.select_tab = {
desc = "Open the entry under the cursor in a new tab", desc = 'Open the entry under the cursor in a new tab',
deprecated = true, deprecated = true,
callback = function() callback = function()
oil.select({ tab = true }) oil.select({ tab = true })
@ -68,25 +68,25 @@ M.select_tab = {
} }
M.preview = { M.preview = {
desc = "Open the entry under the cursor in a preview window, or close the preview window if already open", desc = 'Open the entry under the cursor in a preview window, or close the preview window if already open',
parameters = { parameters = {
vertical = { vertical = {
type = "boolean", type = 'boolean',
desc = "Open the buffer in a vertical split", desc = 'Open the buffer in a vertical split',
}, },
horizontal = { horizontal = {
type = "boolean", type = 'boolean',
desc = "Open the buffer in a horizontal split", desc = 'Open the buffer in a horizontal split',
}, },
split = { split = {
type = '"aboveleft"|"belowright"|"topleft"|"botright"', type = '"aboveleft"|"belowright"|"topleft"|"botright"',
desc = "Split modifier", desc = 'Split modifier',
}, },
}, },
callback = function(opts) callback = function(opts)
local entry = oil.get_cursor_entry() local entry = oil.get_cursor_entry()
if not entry then if not entry then
vim.notify("Could not find entry under cursor", vim.log.levels.ERROR) vim.notify('Could not find entry under cursor', vim.log.levels.ERROR)
return return
end end
local winid = util.get_preview_win() local winid = util.get_preview_win()
@ -95,7 +95,7 @@ M.preview = {
if entry.id == cur_id then if entry.id == cur_id then
vim.api.nvim_win_close(winid, true) vim.api.nvim_win_close(winid, true)
if util.is_floating_win() then if util.is_floating_win() then
local layout = require("oil.layout") local layout = require('oil.layout')
local win_opts = layout.get_fullscreen_win_opts() local win_opts = layout.get_fullscreen_win_opts()
vim.api.nvim_win_set_config(0, win_opts) vim.api.nvim_win_set_config(0, win_opts)
end end
@ -107,13 +107,13 @@ M.preview = {
} }
M.preview_scroll_down = { M.preview_scroll_down = {
desc = "Scroll down in the preview window", desc = 'Scroll down in the preview window',
callback = function() callback = function()
local winid = util.get_preview_win() local winid = util.get_preview_win()
if winid then if winid then
vim.api.nvim_win_call(winid, function() vim.api.nvim_win_call(winid, function()
vim.cmd.normal({ vim.cmd.normal({
args = { vim.api.nvim_replace_termcodes("<C-d>", true, true, true) }, args = { vim.api.nvim_replace_termcodes('<C-d>', true, true, true) },
bang = true, bang = true,
}) })
end) end)
@ -122,13 +122,13 @@ M.preview_scroll_down = {
} }
M.preview_scroll_up = { M.preview_scroll_up = {
desc = "Scroll up in the preview window", desc = 'Scroll up in the preview window',
callback = function() callback = function()
local winid = util.get_preview_win() local winid = util.get_preview_win()
if winid then if winid then
vim.api.nvim_win_call(winid, function() vim.api.nvim_win_call(winid, function()
vim.cmd.normal({ vim.cmd.normal({
args = { vim.api.nvim_replace_termcodes("<C-u>", true, true, true) }, args = { vim.api.nvim_replace_termcodes('<C-u>', true, true, true) },
bang = true, bang = true,
}) })
end) end)
@ -137,50 +137,50 @@ M.preview_scroll_up = {
} }
M.preview_scroll_left = { M.preview_scroll_left = {
desc = "Scroll left in the preview window", desc = 'Scroll left in the preview window',
callback = function() callback = function()
local winid = util.get_preview_win() local winid = util.get_preview_win()
if winid then if winid then
vim.api.nvim_win_call(winid, function() vim.api.nvim_win_call(winid, function()
vim.cmd.normal({ "zH", bang = true }) vim.cmd.normal({ 'zH', bang = true })
end) end)
end end
end, end,
} }
M.preview_scroll_right = { M.preview_scroll_right = {
desc = "Scroll right in the preview window", desc = 'Scroll right in the preview window',
callback = function() callback = function()
local winid = util.get_preview_win() local winid = util.get_preview_win()
if winid then if winid then
vim.api.nvim_win_call(winid, function() vim.api.nvim_win_call(winid, function()
vim.cmd.normal({ "zL", bang = true }) vim.cmd.normal({ 'zL', bang = true })
end) end)
end 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,
} }
M.close = { M.close = {
desc = "Close oil and restore original buffer", desc = 'Close oil and restore original buffer',
callback = function(opts) callback = function(opts)
opts = opts or {} opts = opts or {}
oil.close(opts) oil.close(opts)
end, end,
parameters = { parameters = {
exit_if_last_buf = { exit_if_last_buf = {
type = "boolean", type = 'boolean',
desc = "Exit vim if oil is closed as the last buffer", desc = 'Exit vim if oil is closed as the last buffer',
}, },
}, },
} }
M.close_float = { M.close_float = {
desc = "Close oil if the window is floating, otherwise do nothing", desc = 'Close oil if the window is floating, otherwise do nothing',
callback = function(opts) callback = function(opts)
if vim.w.is_oil_win then if vim.w.is_oil_win then
opts = opts or {} opts = opts or {}
@ -189,8 +189,8 @@ M.close_float = {
end, end,
parameters = { parameters = {
exit_if_last_buf = { exit_if_last_buf = {
type = "boolean", type = 'boolean',
desc = "Exit vim if oil is closed as the last buffer", desc = 'Exit vim if oil is closed as the last buffer',
}, },
}, },
} }
@ -202,42 +202,42 @@ local function cd(cmd, silent)
if dir then if dir then
vim.cmd({ cmd = cmd, args = { dir } }) vim.cmd({ cmd = cmd, args = { dir } })
if not silent then if not silent then
vim.notify(string.format("CWD: %s", dir), vim.log.levels.INFO) vim.notify(string.format('CWD: %s', dir), vim.log.levels.INFO)
end end
else else
vim.notify("Cannot :cd; not in a directory", vim.log.levels.WARN) vim.notify('Cannot :cd; not in a directory', vim.log.levels.WARN)
end end
end end
M.cd = { M.cd = {
desc = ":cd to the current oil directory", desc = ':cd to the current oil directory',
callback = function(opts) callback = function(opts)
opts = opts or {} opts = opts or {}
local cmd = "cd" local cmd = 'cd'
if opts.scope == "tab" then if opts.scope == 'tab' then
cmd = "tcd" cmd = 'tcd'
elseif opts.scope == "win" then elseif opts.scope == 'win' then
cmd = "lcd" cmd = 'lcd'
end end
cd(cmd, opts.silent) cd(cmd, opts.silent)
end, end,
parameters = { parameters = {
scope = { scope = {
type = 'nil|"tab"|"win"', type = 'nil|"tab"|"win"',
desc = "Scope of the directory change (e.g. use |:tcd| or |:lcd|)", desc = 'Scope of the directory change (e.g. use |:tcd| or |:lcd|)',
}, },
silent = { silent = {
type = "boolean", type = 'boolean',
desc = "Do not show a message when changing directories", desc = 'Do not show a message when changing directories',
}, },
}, },
} }
M.tcd = { M.tcd = {
desc = ":tcd to the current oil directory", desc = ':tcd to the current oil directory',
deprecated = true, deprecated = true,
callback = function() callback = function()
cd("tcd") cd('tcd')
end, end,
} }
@ -249,46 +249,46 @@ M.open_cwd = {
} }
M.toggle_hidden = { M.toggle_hidden = {
desc = "Toggle hidden files and directories", desc = 'Toggle hidden files and directories',
callback = function() callback = function()
require("oil.view").toggle_hidden() require('oil.view').toggle_hidden()
end, end,
} }
M.open_terminal = { M.open_terminal = {
desc = "Open a terminal in the current directory", desc = 'Open a terminal in the current directory',
callback = function() callback = function()
local config = require("oil.config") local config = require('oil.config')
local bufname = vim.api.nvim_buf_get_name(0) local bufname = vim.api.nvim_buf_get_name(0)
local adapter = config.get_adapter_by_scheme(bufname) local adapter = config.get_adapter_by_scheme(bufname)
if not adapter then if not adapter then
return return
end end
if adapter.name == "files" then if adapter.name == 'files' then
local dir = oil.get_current_dir() local dir = oil.get_current_dir()
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 if vim.fn.has('nvim-0.11') == 1 then
vim.fn.jobstart(vim.o.shell, { cwd = dir, term = true }) vim.fn.jobstart(vim.o.shell, { cwd = dir, term = true })
else else
---@diagnostic disable-next-line: deprecated ---@diagnostic disable-next-line: deprecated
vim.fn.termopen(vim.o.shell, { cwd = dir }) vim.fn.termopen(vim.o.shell, { cwd = dir })
end 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 local term_id
if vim.fn.has("nvim-0.11") == 1 then if vim.fn.has('nvim-0.11') == 1 then
term_id = vim.fn.jobstart(cmd, { term = true }) term_id = vim.fn.jobstart(cmd, { term = true })
else else
---@diagnostic disable-next-line: deprecated ---@diagnostic disable-next-line: deprecated
term_id = vim.fn.termopen(cmd) term_id = vim.fn.termopen(cmd)
end 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
else else
vim.notify( vim.notify(
@ -304,25 +304,25 @@ M.open_terminal = {
---@return nil|string[] cmd ---@return nil|string[] cmd
---@return nil|string error ---@return nil|string error
local function get_open_cmd(path) local function get_open_cmd(path)
if vim.fn.has("mac") == 1 then if vim.fn.has('mac') == 1 then
return { "open", path } return { 'open', path }
elseif vim.fn.has("win32") == 1 then elseif vim.fn.has('win32') == 1 then
if vim.fn.executable("rundll32") == 1 then if vim.fn.executable('rundll32') == 1 then
return { "rundll32", "url.dll,FileProtocolHandler", path } return { 'rundll32', 'url.dll,FileProtocolHandler', path }
else else
return nil, "rundll32 not found" return nil, 'rundll32 not found'
end end
elseif vim.fn.executable("explorer.exe") == 1 then elseif vim.fn.executable('explorer.exe') == 1 then
return { "explorer.exe", path } return { 'explorer.exe', path }
elseif vim.fn.executable("xdg-open") == 1 then elseif vim.fn.executable('xdg-open') == 1 then
return { "xdg-open", path } return { 'xdg-open', path }
else else
return nil, "no handler found" return nil, 'no handler found'
end end
end end
M.open_external = { M.open_external = {
desc = "Open the entry under the cursor in an external program", desc = 'Open the entry under the cursor in an external program',
callback = function() callback = function()
local entry = oil.get_cursor_entry() local entry = oil.get_cursor_entry()
local dir = oil.get_current_dir() local dir = oil.get_current_dir()
@ -338,20 +338,20 @@ M.open_external = {
local cmd, err = get_open_cmd(path) local cmd, err = get_open_cmd(path)
if not cmd then if not cmd then
vim.notify(string.format("Could not open %s: %s", path, err), vim.log.levels.ERROR) vim.notify(string.format('Could not open %s: %s', path, err), vim.log.levels.ERROR)
return return
end end
local jid = vim.fn.jobstart(cmd, { detach = true }) local jid = vim.fn.jobstart(cmd, { detach = true })
assert(jid > 0, "Failed to start job") assert(jid > 0, 'Failed to start job')
end, end,
} }
M.refresh = { M.refresh = {
desc = "Refresh current directory list", desc = 'Refresh current directory list',
callback = function(opts) callback = function(opts)
opts = opts or {} opts = opts or {}
if vim.bo.modified and not opts.force then if vim.bo.modified and not opts.force then
local ok, choice = pcall(vim.fn.confirm, "Discard changes?", "No\nYes") local ok, choice = pcall(vim.fn.confirm, 'Discard changes?', 'No\nYes')
if not ok or choice ~= 2 then if not ok or choice ~= 2 then
return return
end end
@ -363,26 +363,26 @@ M.refresh = {
end, end,
parameters = { parameters = {
force = { force = {
desc = "When true, do not prompt user if they will be discarding changes", desc = 'When true, do not prompt user if they will be discarding changes',
type = "boolean", type = 'boolean',
}, },
}, },
} }
local function open_cmdline_with_path(path) local function open_cmdline_with_path(path)
local escaped = local escaped =
vim.api.nvim_replace_termcodes(": " .. vim.fn.fnameescape(path) .. "<Home>", true, false, true) vim.api.nvim_replace_termcodes(': ' .. vim.fn.fnameescape(path) .. '<Home>', true, false, true)
vim.api.nvim_feedkeys(escaped, "n", false) vim.api.nvim_feedkeys(escaped, 'n', false)
end end
M.open_cmdline = { M.open_cmdline = {
desc = "Open vim cmdline with current entry as an argument", desc = 'Open vim cmdline with current entry as an argument',
callback = function(opts) callback = function(opts)
opts = vim.tbl_deep_extend("keep", opts or {}, { opts = vim.tbl_deep_extend('keep', opts or {}, {
shorten_path = true, shorten_path = true,
}) })
local config = require("oil.config") local config = require('oil.config')
local fs = require("oil.fs") local fs = require('oil.fs')
local entry = oil.get_cursor_entry() local entry = oil.get_cursor_entry()
if not entry then if not entry then
return return
@ -393,7 +393,7 @@ M.open_cmdline = {
return return
end end
local adapter = config.get_adapter_by_scheme(scheme) local adapter = config.get_adapter_by_scheme(scheme)
if not adapter or not path or adapter.name ~= "files" then if not adapter or not path or adapter.name ~= 'files' then
return return
end end
local fullpath = fs.posix_to_os_path(path) .. entry.name local fullpath = fs.posix_to_os_path(path) .. entry.name
@ -407,18 +407,18 @@ M.open_cmdline = {
end, end,
parameters = { parameters = {
modify = { modify = {
desc = "Modify the path with |fnamemodify()| using this as the mods argument", desc = 'Modify the path with |fnamemodify()| using this as the mods argument',
type = "string", type = 'string',
}, },
shorten_path = { shorten_path = {
desc = "Use relative paths when possible", desc = 'Use relative paths when possible',
type = "boolean", type = 'boolean',
}, },
}, },
} }
M.yank_entry = { M.yank_entry = {
desc = "Yank the filepath of the entry under the cursor to a register", desc = 'Yank the filepath of the entry under the cursor to a register',
callback = function(opts) callback = function(opts)
opts = opts or {} opts = opts or {}
local entry = oil.get_cursor_entry() local entry = oil.get_cursor_entry()
@ -427,8 +427,8 @@ M.yank_entry = {
return return
end end
local name = entry.name local name = entry.name
if entry.type == "directory" then if entry.type == 'directory' then
name = name .. "/" name = name .. '/'
end end
local path = dir .. name local path = dir .. name
if opts.modify then if opts.modify then
@ -438,14 +438,14 @@ M.yank_entry = {
end, end,
parameters = { parameters = {
modify = { modify = {
desc = "Modify the path with |fnamemodify()| using this as the mods argument", desc = 'Modify the path with |fnamemodify()| using this as the mods argument',
type = "string", type = 'string',
}, },
}, },
} }
M.copy_entry_path = { M.copy_entry_path = {
desc = "Yank the filepath of the entry under the cursor to a register", desc = 'Yank the filepath of the entry under the cursor to a register',
deprecated = true, deprecated = true,
callback = function() callback = function()
local entry = oil.get_cursor_entry() local entry = oil.get_cursor_entry()
@ -458,7 +458,7 @@ M.copy_entry_path = {
} }
M.copy_entry_filename = { M.copy_entry_filename = {
desc = "Yank the filename of the entry under the cursor to a register", desc = 'Yank the filename of the entry under the cursor to a register',
deprecated = true, deprecated = true,
callback = function() callback = function()
local entry = oil.get_cursor_entry() local entry = oil.get_cursor_entry()
@ -470,30 +470,30 @@ M.copy_entry_filename = {
} }
M.copy_to_system_clipboard = { M.copy_to_system_clipboard = {
desc = "Copy the entry under the cursor to the system clipboard", desc = 'Copy the entry under the cursor to the system clipboard',
callback = function() callback = function()
require("oil.clipboard").copy_to_system_clipboard() require('oil.clipboard').copy_to_system_clipboard()
end, end,
} }
M.paste_from_system_clipboard = { M.paste_from_system_clipboard = {
desc = "Paste the system clipboard into the current oil directory", desc = 'Paste the system clipboard into the current oil directory',
callback = function(opts) callback = function(opts)
require("oil.clipboard").paste_from_system_clipboard(opts and opts.delete_original) require('oil.clipboard').paste_from_system_clipboard(opts and opts.delete_original)
end, end,
parameters = { parameters = {
delete_original = { delete_original = {
type = "boolean", type = 'boolean',
desc = "Delete the original file after copying", 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,
callback = function() callback = function()
local fs = require("oil.fs") local fs = require('oil.fs')
local dir = oil.get_current_dir() local dir = oil.get_current_dir()
if dir then if dir then
open_cmdline_with_path(fs.shorten_path(dir)) open_cmdline_with_path(fs.shorten_path(dir))
@ -502,7 +502,7 @@ M.open_cmdline_dir = {
} }
M.change_sort = { M.change_sort = {
desc = "Change the sort order", desc = 'Change the sort order',
callback = function(opts) callback = function(opts)
opts = opts or {} opts = opts or {}
@ -511,21 +511,21 @@ M.change_sort = {
return return
end end
local sort_cols = { "name", "size", "atime", "mtime", "ctime", "birthtime" } local sort_cols = { 'name', 'size', 'atime', 'mtime', 'ctime', 'birthtime' }
vim.ui.select(sort_cols, { prompt = "Sort by", kind = "oil_sort_col" }, function(col) vim.ui.select(sort_cols, { prompt = 'Sort by', kind = 'oil_sort_col' }, function(col)
if not col then if not col then
return return
end end
vim.ui.select( vim.ui.select(
{ "ascending", "descending" }, { 'ascending', 'descending' },
{ prompt = "Sort order", kind = "oil_sort_order" }, { prompt = 'Sort order', kind = 'oil_sort_order' },
function(order) function(order)
if not order then if not order then
return return
end end
order = order == "ascending" and "asc" or "desc" order = order == 'ascending' and 'asc' or 'desc'
oil.set_sort({ oil.set_sort({
{ "type", "asc" }, { 'type', 'asc' },
{ col, order }, { col, order },
}) })
end end
@ -534,24 +534,24 @@ M.change_sort = {
end, end,
parameters = { parameters = {
sort = { sort = {
type = "oil.SortSpec[]", type = 'oil.SortSpec[]',
desc = "List of columns plus direction (see |oil.set_sort|) instead of interactive selection", desc = 'List of columns plus direction (see |oil.set_sort|) instead of interactive selection',
}, },
}, },
} }
M.toggle_trash = { M.toggle_trash = {
desc = "Jump to and from the trash for the current directory", desc = 'Jump to and from the trash for the current directory',
callback = function() callback = function()
local fs = require("oil.fs") local fs = require('oil.fs')
local bufname = vim.api.nvim_buf_get_name(0) local bufname = vim.api.nvim_buf_get_name(0)
local scheme, path = util.parse_url(bufname) local scheme, path = util.parse_url(bufname)
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local url local url
if scheme == "oil://" then if scheme == 'oil://' then
url = "oil-trash://" .. path url = 'oil-trash://' .. path
elseif scheme == "oil-trash://" then elseif scheme == 'oil-trash://' then
url = "oil://" .. path url = 'oil://' .. path
-- The non-linux trash implementations don't support per-directory trash, -- The non-linux trash implementations don't support per-directory trash,
-- so jump back to the stored source buffer. -- so jump back to the stored source buffer.
if not fs.is_linux then if not fs.is_linux then
@ -561,7 +561,7 @@ M.toggle_trash = {
end end
end end
else else
vim.notify("No trash found for buffer", vim.log.levels.WARN) vim.notify('No trash found for buffer', vim.log.levels.WARN)
return return
end end
vim.cmd.edit({ args = { url } }) vim.cmd.edit({ args = { url } })
@ -570,11 +570,11 @@ M.toggle_trash = {
} }
M.send_to_qflist = { M.send_to_qflist = {
desc = "Sends files in the current oil directory to the quickfix list, replacing the previous entries.", desc = 'Sends files in the current oil directory to the quickfix list, replacing the previous entries.',
callback = function(opts) callback = function(opts)
opts = vim.tbl_deep_extend("keep", opts or {}, { opts = vim.tbl_deep_extend('keep', opts or {}, {
target = "qflist", target = 'qflist',
action = "r", action = 'r',
only_matching_search = false, only_matching_search = false,
}) })
util.send_to_quickfix({ util.send_to_quickfix({
@ -586,48 +586,48 @@ M.send_to_qflist = {
parameters = { parameters = {
target = { target = {
type = '"qflist"|"loclist"', type = '"qflist"|"loclist"',
desc = "The target list to send files to", desc = 'The target list to send files to',
}, },
action = { action = {
type = '"r"|"a"', type = '"r"|"a"',
desc = "Replace or add to current quickfix list (see |setqflist-action|)", desc = 'Replace or add to current quickfix list (see |setqflist-action|)',
}, },
only_matching_search = { only_matching_search = {
type = "boolean", type = 'boolean',
desc = "Whether to only add the files that matches the last search. This option only applies when search highlighting is active", desc = 'Whether to only add the files that matches the last search. This option only applies when search highlighting is active',
}, },
}, },
} }
M.add_to_qflist = { M.add_to_qflist = {
desc = "Adds files in the current oil directory to the quickfix list, keeping the previous entries.", desc = 'Adds files in the current oil directory to the quickfix list, keeping the previous entries.',
deprecated = true, deprecated = true,
callback = function() callback = function()
util.send_to_quickfix({ util.send_to_quickfix({
target = "qflist", target = 'qflist',
mode = "a", mode = 'a',
}) })
end, end,
} }
M.send_to_loclist = { M.send_to_loclist = {
desc = "Sends files in the current oil directory to the location list, replacing the previous entries.", desc = 'Sends files in the current oil directory to the location list, replacing the previous entries.',
deprecated = true, deprecated = true,
callback = function() callback = function()
util.send_to_quickfix({ util.send_to_quickfix({
target = "loclist", target = 'loclist',
mode = "r", mode = 'r',
}) })
end, end,
} }
M.add_to_loclist = { M.add_to_loclist = {
desc = "Adds files in the current oil directory to the location list, keeping the previous entries.", desc = 'Adds files in the current oil directory to the location list, keeping the previous entries.',
deprecated = true, deprecated = true,
callback = function() callback = function()
util.send_to_quickfix({ util.send_to_quickfix({
target = "loclist", target = 'loclist',
mode = "a", mode = 'a',
}) })
end, end,
} }
@ -637,7 +637,7 @@ M.add_to_loclist = {
M._get_actions = function() M._get_actions = function()
local ret = {} local ret = {}
for name, action in pairs(M) do for name, action in pairs(M) do
if type(action) == "table" and action.desc then if type(action) == 'table' and action.desc then
table.insert(ret, { table.insert(ret, {
name = name, name = name,
desc = action.desc, desc = action.desc,

View file

@ -1,12 +1,12 @@
local cache = require("oil.cache") local cache = require('oil.cache')
local columns = require("oil.columns") local columns = require('oil.columns')
local config = require("oil.config") local config = require('oil.config')
local constants = require("oil.constants") local constants = require('oil.constants')
local fs = require("oil.fs") local 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 util = require("oil.util") local util = require('oil.util')
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
local M = {} local M = {}
@ -25,7 +25,7 @@ local function read_link_data(path, cb)
assert(link) assert(link)
local stat_path = link local stat_path = link
if not fs.is_absolute(link) then if not fs.is_absolute(link) then
stat_path = fs.join(vim.fn.fnamemodify(path, ":h"), link) stat_path = fs.join(vim.fn.fnamemodify(path, ':h'), link)
end end
uv.fs_stat(stat_path, function(stat_err, stat) uv.fs_stat(stat_path, function(stat_err, stat)
cb(nil, link, stat) cb(nil, link, stat)
@ -43,7 +43,7 @@ end
---@return string ---@return string
M.to_short_os_path = function(path, entry_type) M.to_short_os_path = function(path, entry_type)
local shortpath = fs.shorten_path(fs.posix_to_os_path(path)) local shortpath = fs.shorten_path(fs.posix_to_os_path(path))
if entry_type == "directory" then if entry_type == 'directory' then
shortpath = util.addslash(shortpath, true) shortpath = util.addslash(shortpath, true)
end end
return shortpath return shortpath
@ -61,13 +61,13 @@ file_columns.size = {
return columns.EMPTY return columns.EMPTY
end end
if stat.size >= 1e9 then if stat.size >= 1e9 then
return string.format("%.1fG", stat.size / 1e9) return string.format('%.1fG', stat.size / 1e9)
elseif stat.size >= 1e6 then elseif stat.size >= 1e6 then
return string.format("%.1fM", stat.size / 1e6) return string.format('%.1fM', stat.size / 1e6)
elseif stat.size >= 1e3 then elseif stat.size >= 1e3 then
return string.format("%.1fk", stat.size / 1e3) return string.format('%.1fk', stat.size / 1e3)
else else
return string.format("%d", stat.size) return string.format('%d', stat.size)
end end
end, end,
@ -82,7 +82,7 @@ file_columns.size = {
end, end,
parse = function(line, conf) parse = function(line, conf)
return line:match("^(%d+%S*)%s+(.*)$") return line:match('^(%d+%S*)%s+(.*)$')
end, end,
} }
@ -120,7 +120,7 @@ if not fs.is_windows then
local _, path = util.parse_url(action.url) local _, path = util.parse_url(action.url)
assert(path) assert(path)
return string.format( return string.format(
"CHMOD %s %s", 'CHMOD %s %s',
permissions.mode_to_octal_str(action.value), permissions.mode_to_octal_str(action.value),
M.to_short_os_path(path, action.entry_type) M.to_short_os_path(path, action.entry_type)
) )
@ -147,10 +147,10 @@ end
local current_year local current_year
-- Make sure we run this import-time effect in the main loop (mostly for tests) -- Make sure we run this import-time effect in the main loop (mostly for tests)
vim.schedule(function() vim.schedule(function()
current_year = vim.fn.strftime("%Y") current_year = vim.fn.strftime('%Y')
end) end)
for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do for _, time_key in ipairs({ 'ctime', 'mtime', 'atime', 'birthtime' }) do
file_columns[time_key] = { file_columns[time_key] = {
require_stat = true, require_stat = true,
@ -165,11 +165,11 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
if fmt then if fmt then
ret = vim.fn.strftime(fmt, stat[time_key].sec) ret = vim.fn.strftime(fmt, stat[time_key].sec)
else else
local year = vim.fn.strftime("%Y", stat[time_key].sec) local year = vim.fn.strftime('%Y', stat[time_key].sec)
if year ~= current_year then if year ~= current_year then
ret = vim.fn.strftime("%b %d %Y", stat[time_key].sec) ret = vim.fn.strftime('%b %d %Y', stat[time_key].sec)
else else
ret = vim.fn.strftime("%b %d %H:%M", stat[time_key].sec) ret = vim.fn.strftime('%b %d %H:%M', stat[time_key].sec)
end end
end end
return ret return ret
@ -183,20 +183,20 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
-- 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 pattern = fmt
:gsub("%%.", "%%S+") :gsub('%%.', '%%S+')
:gsub("%s+", "%%s+") :gsub('%s+', '%%s+')
-- escape `()[]` because those are special characters in Lua patterns -- escape `()[]` because those are special characters in Lua patterns
:gsub( :gsub(
"%(", '%(',
"%%(" '%%('
) )
:gsub("%)", "%%)") :gsub('%)', '%%)')
: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
return line:match("^(" .. pattern .. ")%s+(.+)$") return line:match('^(' .. pattern .. ')%s+(.+)$')
end, end,
get_sort_value = function(entry) get_sort_value = function(entry)
@ -238,17 +238,17 @@ M.normalize_url = function(url, callback)
assert(path) assert(path)
if fs.is_windows then if fs.is_windows then
if path == "/" then if path == '/' then
return callback(url) return callback(url)
else else
local is_root_drive = path:match("^/%u$") local is_root_drive = path:match('^/%u$')
if is_root_drive then if is_root_drive then
return callback(url .. "/") return callback(url .. '/')
end end
end end
end end
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p") local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ':p')
uv.fs_realpath(os_path, function(err, new_os_path) uv.fs_realpath(os_path, function(err, new_os_path)
local realpath local realpath
if fs.is_windows then if fs.is_windows then
@ -264,8 +264,8 @@ M.normalize_url = function(url, callback)
vim.schedule_wrap(function(stat_err, stat) vim.schedule_wrap(function(stat_err, stat)
local is_directory local is_directory
if stat then if stat then
is_directory = stat.type == "directory" is_directory = stat.type == 'directory'
elseif vim.endswith(realpath, "/") or (fs.is_windows and vim.endswith(realpath, "\\")) then elseif vim.endswith(realpath, '/') or (fs.is_windows and vim.endswith(realpath, '\\')) then
is_directory = true is_directory = true
else else
local filetype = vim.filetype.match({ filename = vim.fs.basename(realpath) }) local filetype = vim.filetype.match({ filename = vim.fs.basename(realpath) })
@ -276,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(vim.fn.fnamemodify(realpath, ":.")) callback(vim.fn.fnamemodify(realpath, ':.'))
end end
end) end)
) )
@ -292,11 +292,11 @@ M.get_entry_path = function(url, entry, cb)
local scheme, path = util.parse_url(parent_url) local scheme, path = util.parse_url(parent_url)
M.normalize_url(scheme .. path .. entry.name, cb) M.normalize_url(scheme .. path .. entry.name, cb)
else else
if entry.type == "directory" then if entry.type == 'directory' then
cb(url) cb(url)
else else
local _, path = util.parse_url(url) local _, path = util.parse_url(url)
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(assert(path)), ":p") local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(assert(path)), ':p')
cb(os_path) cb(os_path)
end end
end end
@ -321,10 +321,10 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
end end
-- Make sure we always get fs_stat info for links -- Make sure we always get fs_stat info for links
if entry[FIELD_TYPE] == "link" then if entry[FIELD_TYPE] == 'link' then
read_link_data(entry_path, function(link_err, link, link_stat) read_link_data(entry_path, function(link_err, link, link_stat)
if link_err then if link_err then
log.warn("Error reading link data %s: %s", entry_path, link_err) log.warn('Error reading link data %s: %s', entry_path, link_err)
return cb() return cb()
end end
meta.link = link meta.link = link
@ -336,7 +336,7 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
-- The link is broken, so let's use the stat of the link itself -- The link is broken, so let's use the stat of the link itself
uv.fs_lstat(entry_path, function(stat_err, stat) uv.fs_lstat(entry_path, function(stat_err, stat)
if stat_err then if stat_err then
log.warn("Error lstat link file %s: %s", entry_path, stat_err) log.warn('Error lstat link file %s: %s', entry_path, stat_err)
return cb() return cb()
end end
meta.stat = stat meta.stat = stat
@ -350,7 +350,7 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
elseif require_stat then elseif require_stat then
uv.fs_stat(entry_path, function(stat_err, stat) uv.fs_stat(entry_path, function(stat_err, stat)
if stat_err then if stat_err then
log.warn("Error stat file %s: %s", entry_path, stat_err) log.warn('Error stat file %s: %s', entry_path, stat_err)
return cb() return cb()
end end
assert(stat) assert(stat)
@ -368,11 +368,11 @@ end
if fs.is_windows then if fs.is_windows then
local old_fetch_metadata = fetch_entry_metadata local old_fetch_metadata = fetch_entry_metadata
fetch_entry_metadata = function(parent_dir, entry, require_stat, cb) fetch_entry_metadata = function(parent_dir, entry, require_stat, cb)
if entry[FIELD_TYPE] == "link" then if entry[FIELD_TYPE] == 'link' then
local entry_path = fs.posix_to_os_path(parent_dir .. entry[FIELD_NAME]) local entry_path = fs.posix_to_os_path(parent_dir .. entry[FIELD_NAME])
uv.fs_lstat(entry_path, function(stat_err, stat) uv.fs_lstat(entry_path, function(stat_err, stat)
if stat_err then if stat_err then
log.warn("Error lstat link file %s: %s", entry_path, stat_err) log.warn('Error lstat link file %s: %s', entry_path, stat_err)
return old_fetch_metadata(parent_dir, entry, require_stat, cb) return old_fetch_metadata(parent_dir, entry, require_stat, cb)
end end
assert(stat) assert(stat)
@ -398,17 +398,17 @@ local function list_windows_drives(url, column_defs, cb)
local _, path = util.parse_url(url) local _, path = util.parse_url(url)
assert(path) assert(path)
local require_stat = columns_require_stat(column_defs) local require_stat = columns_require_stat(column_defs)
local stdout = "" local stdout = ''
local jid = vim.fn.jobstart({ "wmic", "logicaldisk", "get", "name" }, { local jid = vim.fn.jobstart({ 'wmic', 'logicaldisk', 'get', 'name' }, {
stdout_buffered = true, stdout_buffered = true,
on_stdout = function(_, data) on_stdout = function(_, data)
stdout = table.concat(data, "\n") stdout = table.concat(data, '\n')
end, end,
on_exit = function(_, code) on_exit = function(_, code)
if code ~= 0 then if code ~= 0 then
return cb("Error listing windows devices") return cb('Error listing windows devices')
end end
local lines = vim.split(stdout, "\n", { plain = true, trimempty = true }) local lines = vim.split(stdout, '\n', { plain = true, trimempty = true })
-- Remove the "Name" header -- Remove the "Name" header
table.remove(lines, 1) table.remove(lines, 1)
local internal_entries = {} local internal_entries = {}
@ -421,12 +421,12 @@ local function list_windows_drives(url, column_defs, cb)
end) end)
for _, disk in ipairs(lines) do for _, disk in ipairs(lines) do
if disk:match("^%s*$") then if disk:match('^%s*$') then
-- Skip empty line -- Skip empty line
complete_disk_cb() complete_disk_cb()
else else
disk = disk:gsub(":%s*$", "") disk = disk:gsub(':%s*$', '')
local cache_entry = cache.create_entry(url, disk, "directory") local cache_entry = cache.create_entry(url, disk, 'directory')
table.insert(internal_entries, cache_entry) table.insert(internal_entries, cache_entry)
fetch_entry_metadata(path, cache_entry, require_stat, complete_disk_cb) fetch_entry_metadata(path, cache_entry, require_stat, complete_disk_cb)
end end
@ -434,7 +434,7 @@ local function list_windows_drives(url, column_defs, cb)
end, end,
}) })
if jid <= 0 then if jid <= 0 then
cb("Could not list windows devices") cb('Could not list windows devices')
end end
end end
@ -444,7 +444,7 @@ end
M.list = function(url, column_defs, cb) M.list = function(url, column_defs, cb)
local _, path = util.parse_url(url) local _, path = util.parse_url(url)
assert(path) assert(path)
if fs.is_windows and path == "/" then if fs.is_windows and path == '/' then
return list_windows_drives(url, column_defs, cb) return list_windows_drives(url, column_defs, cb)
end end
local dir = fs.posix_to_os_path(path) local dir = fs.posix_to_os_path(path)
@ -453,7 +453,7 @@ M.list = function(url, column_defs, cb)
---@diagnostic disable-next-line: param-type-mismatch, discard-returns ---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(dir, function(open_err, fd) uv.fs_opendir(dir, function(open_err, fd)
if open_err then if open_err then
if open_err:match("^ENOENT: no such file or directory") then if open_err:match('^ENOENT: no such file or directory') then
-- If the directory doesn't exist, treat the list as a success. We will be able to traverse -- If the directory doesn't exist, treat the list as a success. We will be able to traverse
-- and edit a not-yet-existing directory. -- and edit a not-yet-existing directory.
return cb() return cb()
@ -505,7 +505,7 @@ M.is_modifiable = function(bufnr)
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local _, path = util.parse_url(bufname) local _, path = util.parse_url(bufname)
assert(path) assert(path)
if fs.is_windows and path == "/" then if fs.is_windows and path == '/' then
return false return false
end end
local dir = fs.posix_to_os_path(path) local dir = fs.posix_to_os_path(path)
@ -515,30 +515,30 @@ M.is_modifiable = function(bufnr)
end end
-- fs_access can return nil, force boolean return -- fs_access can return nil, force boolean return
return uv.fs_access(dir, "W") == true return uv.fs_access(dir, 'W') == true
end end
---@param action oil.Action ---@param action oil.Action
---@return string ---@return string
M.render_action = function(action) M.render_action = function(action)
if action.type == "create" then if action.type == 'create' then
local _, path = util.parse_url(action.url) local _, path = util.parse_url(action.url)
assert(path) assert(path)
local ret = string.format("CREATE %s", M.to_short_os_path(path, action.entry_type)) local ret = string.format('CREATE %s', M.to_short_os_path(path, action.entry_type))
if action.link then if action.link then
ret = ret .. " -> " .. fs.posix_to_os_path(action.link) ret = ret .. ' -> ' .. fs.posix_to_os_path(action.link)
end end
return ret return ret
elseif action.type == "delete" then elseif action.type == 'delete' then
local _, path = util.parse_url(action.url) local _, path = util.parse_url(action.url)
assert(path) assert(path)
local short_path = M.to_short_os_path(path, action.entry_type) local short_path = M.to_short_os_path(path, action.entry_type)
if config.delete_to_trash then if config.delete_to_trash then
return string.format(" TRASH %s", short_path) return string.format(' TRASH %s', short_path)
else else
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 = assert(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)
@ -546,7 +546,7 @@ M.render_action = function(action)
local _, dest_path = util.parse_url(action.dest_url) local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path) assert(dest_path)
return string.format( return string.format(
" %s %s -> %s", ' %s %s -> %s',
action.type:upper(), action.type:upper(),
M.to_short_os_path(src_path, action.entry_type), M.to_short_os_path(src_path, action.entry_type),
M.to_short_os_path(dest_path, action.entry_type) M.to_short_os_path(dest_path, action.entry_type)
@ -563,7 +563,7 @@ end
---@param action oil.Action ---@param action oil.Action
---@param cb fun(err: nil|string) ---@param cb fun(err: nil|string)
M.perform_action = function(action, cb) M.perform_action = function(action, cb)
if action.type == "create" then if action.type == 'create' then
local _, path = util.parse_url(action.url) local _, path = util.parse_url(action.url)
assert(path) assert(path)
path = fs.posix_to_os_path(path) path = fs.posix_to_os_path(path)
@ -579,16 +579,16 @@ M.perform_action = function(action, cb)
end) end)
end end
if action.entry_type == "directory" then if action.entry_type == 'directory' then
uv.fs_mkdir(path, config.new_dir_mode, function(err) uv.fs_mkdir(path, config.new_dir_mode, function(err)
-- Ignore if the directory already exists -- Ignore if the directory already exists
if not err or err:match("^EEXIST:") then if not err or err:match('^EEXIST:') then
cb() cb()
else else
cb(err) cb(err)
end end
end) end)
elseif action.entry_type == "link" and action.link then elseif action.entry_type == 'link' and action.link then
local flags = nil local flags = nil
local target = fs.posix_to_os_path(action.link) local target = fs.posix_to_os_path(action.link)
if fs.is_windows then if fs.is_windows then
@ -602,7 +602,7 @@ M.perform_action = function(action, cb)
else else
fs.touch(path, config.new_file_mode, cb) fs.touch(path, config.new_file_mode, cb)
end end
elseif action.type == "delete" then elseif action.type == 'delete' then
local _, path = util.parse_url(action.url) local _, path = util.parse_url(action.url)
assert(path) assert(path)
path = fs.posix_to_os_path(path) path = fs.posix_to_os_path(path)
@ -619,11 +619,11 @@ M.perform_action = function(action, cb)
end end
if config.delete_to_trash then if config.delete_to_trash then
require("oil.adapters.trash").delete_to_trash(path, cb) require('oil.adapters.trash').delete_to_trash(path, cb)
else else
fs.recursive_delete(action.entry_type, path, cb) fs.recursive_delete(action.entry_type, path, cb)
end end
elseif action.type == "move" then elseif action.type == 'move' then
local dest_adapter = assert(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)
@ -641,7 +641,7 @@ M.perform_action = function(action, cb)
-- We should never hit this because we don't implement supported_cross_adapter_actions -- We should never hit this because we don't implement supported_cross_adapter_actions
cb("files adapter doesn't support cross-adapter move") cb("files adapter doesn't support cross-adapter move")
end end
elseif action.type == "copy" then elseif action.type == 'copy' then
local dest_adapter = assert(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)
@ -656,7 +656,7 @@ M.perform_action = function(action, cb)
cb("files adapter doesn't support cross-adapter copy") cb("files adapter doesn't support cross-adapter copy")
end end
else else
cb(string.format("Bad action type: %s", action.type)) cb(string.format('Bad action type: %s', action.type))
end end
end end

View file

@ -4,7 +4,7 @@ local M = {}
---@param num integer ---@param num integer
---@return string ---@return string
local function perm_to_str(exe_modifier, num) local function perm_to_str(exe_modifier, num)
local str = (bit.band(num, 4) ~= 0 and "r" or "-") .. (bit.band(num, 2) ~= 0 and "w" or "-") local str = (bit.band(num, 4) ~= 0 and 'r' or '-') .. (bit.band(num, 2) ~= 0 and 'w' or '-')
if exe_modifier then if exe_modifier then
if bit.band(num, 1) ~= 0 then if bit.band(num, 1) ~= 0 then
return str .. exe_modifier return str .. exe_modifier
@ -12,7 +12,7 @@ local function perm_to_str(exe_modifier, num)
return str .. exe_modifier:upper() return str .. exe_modifier:upper()
end end
else else
return str .. (bit.band(num, 1) ~= 0 and "x" or "-") return str .. (bit.band(num, 1) ~= 0 and 'x' or '-')
end end
end end
@ -20,9 +20,9 @@ end
---@return string ---@return string
M.mode_to_str = function(mode) M.mode_to_str = function(mode)
local extra = bit.rshift(mode, 9) local extra = bit.rshift(mode, 9)
return perm_to_str(bit.band(extra, 4) ~= 0 and "s", bit.rshift(mode, 6)) return perm_to_str(bit.band(extra, 4) ~= 0 and 's', bit.rshift(mode, 6))
.. perm_to_str(bit.band(extra, 2) ~= 0 and "s", bit.rshift(mode, 3)) .. perm_to_str(bit.band(extra, 2) ~= 0 and 's', bit.rshift(mode, 3))
.. perm_to_str(bit.band(extra, 1) ~= 0 and "t", mode) .. perm_to_str(bit.band(extra, 1) ~= 0 and 't', mode)
end end
---@param mode integer ---@param mode integer
@ -38,25 +38,25 @@ end
---@param str string String of 3 characters ---@param str string String of 3 characters
---@return nil|integer ---@return nil|integer
local function str_to_mode(str) local function str_to_mode(str)
local r, w, x = unpack(vim.split(str, "", {})) local r, w, x = unpack(vim.split(str, '', {}))
local mode = 0 local mode = 0
if r == "r" then if r == 'r' then
mode = bit.bor(mode, 4) mode = bit.bor(mode, 4)
elseif r ~= "-" then elseif r ~= '-' then
return nil return nil
end end
if w == "w" then if w == 'w' then
mode = bit.bor(mode, 2) mode = bit.bor(mode, 2)
elseif w ~= "-" then elseif w ~= '-' then
return nil return nil
end end
-- t means sticky and executable -- t means sticky and executable
-- T means sticky, not executable -- T means sticky, not executable
-- s means setuid/setgid and executable -- s means setuid/setgid and executable
-- S means setuid/setgid and not executable -- S means setuid/setgid and not executable
if x == "x" or x == "t" or x == "s" then if x == 'x' or x == 't' or x == 's' then
mode = bit.bor(mode, 1) mode = bit.bor(mode, 1)
elseif x ~= "-" and x ~= "T" and x ~= "S" then elseif x ~= '-' and x ~= 'T' and x ~= 'S' then
return nil return nil
end end
return mode return mode
@ -67,13 +67,13 @@ end
local function parse_extra_bits(perm) local function parse_extra_bits(perm)
perm = perm:lower() perm = perm:lower()
local mode = 0 local mode = 0
if perm:sub(3, 3) == "s" then if perm:sub(3, 3) == 's' then
mode = bit.bor(mode, 4) mode = bit.bor(mode, 4)
end end
if perm:sub(6, 6) == "s" then if perm:sub(6, 6) == 's' then
mode = bit.bor(mode, 2) mode = bit.bor(mode, 2)
end end
if perm:sub(9, 9) == "t" then if perm:sub(9, 9) == 't' then
mode = bit.bor(mode, 1) mode = bit.bor(mode, 1)
end end
return mode return mode
@ -83,7 +83,7 @@ end
---@return nil|integer ---@return nil|integer
---@return nil|string ---@return nil|string
M.parse = function(line) M.parse = function(line)
local strval, rem = line:match("^([r%-][w%-][xsS%-][r%-][w%-][xsS%-][r%-][w%-][xtT%-])%s*(.*)$") local strval, rem = line:match('^([r%-][w%-][xsS%-][r%-][w%-][xsS%-][r%-][w%-][xtT%-])%s*(.*)$')
if not strval then if not strval then
return return
end end

View file

@ -1,11 +1,11 @@
local config = require("oil.config") local config = require('oil.config')
local constants = require("oil.constants") local constants = require('oil.constants')
local files = require("oil.adapters.files") local files = require('oil.adapters.files')
local fs = require("oil.fs") local fs = require('oil.fs')
local loading = require("oil.loading") local loading = require('oil.loading')
local pathutil = require("oil.pathutil") local pathutil = require('oil.pathutil')
local s3fs = require("oil.adapters.s3.s3fs") local s3fs = require('oil.adapters.s3.s3fs')
local util = require("oil.util") local util = require('oil.util')
local M = {} local M = {}
local FIELD_META = constants.FIELD_META local FIELD_META = constants.FIELD_META
@ -21,11 +21,11 @@ M.parse_url = function(oil_url)
local scheme, url = util.parse_url(oil_url) local scheme, url = util.parse_url(oil_url)
assert(scheme and url, string.format("Malformed input url '%s'", oil_url)) assert(scheme and url, string.format("Malformed input url '%s'", oil_url))
local ret = { scheme = scheme } local ret = { scheme = scheme }
local bucket, path = url:match("^([^/]+)/?(.*)$") local bucket, path = url:match('^([^/]+)/?(.*)$')
ret.bucket = bucket ret.bucket = bucket
ret.path = path ~= "" and path or nil ret.path = path ~= '' and path or nil
if not ret.bucket and ret.path then if not ret.bucket and ret.path then
error(string.format("Parsing error for s3 url: %s", oil_url)) error(string.format('Parsing error for s3 url: %s', oil_url))
end end
---@cast ret oil.s3Url ---@cast ret oil.s3Url
return ret return ret
@ -36,43 +36,43 @@ end
local function url_to_str(url) local function url_to_str(url)
local pieces = { url.scheme } local pieces = { url.scheme }
if url.bucket then if url.bucket then
assert(url.bucket ~= "") assert(url.bucket ~= '')
table.insert(pieces, url.bucket) table.insert(pieces, url.bucket)
table.insert(pieces, "/") table.insert(pieces, '/')
end end
if url.path then if url.path then
assert(url.path ~= "") assert(url.path ~= '')
table.insert(pieces, url.path) table.insert(pieces, url.path)
end end
return table.concat(pieces, "") return table.concat(pieces, '')
end end
---@param url oil.s3Url ---@param url oil.s3Url
---@param is_folder boolean ---@param is_folder boolean
---@return string ---@return string
local function url_to_s3(url, is_folder) local function url_to_s3(url, is_folder)
local pieces = { "s3://" } local pieces = { 's3://' }
if url.bucket then if url.bucket then
assert(url.bucket ~= "") assert(url.bucket ~= '')
table.insert(pieces, url.bucket) table.insert(pieces, url.bucket)
table.insert(pieces, "/") table.insert(pieces, '/')
end end
if url.path then if url.path then
assert(url.path ~= "") assert(url.path ~= '')
table.insert(pieces, url.path) table.insert(pieces, url.path)
if is_folder and not vim.endswith(url.path, "/") then if is_folder and not vim.endswith(url.path, '/') then
table.insert(pieces, "/") table.insert(pieces, '/')
end end
end end
return table.concat(pieces, "") return table.concat(pieces, '')
end end
---@param url oil.s3Url ---@param url oil.s3Url
---@return boolean ---@return boolean
local function is_bucket(url) local function is_bucket(url)
assert(url.bucket and url.bucket ~= "") assert(url.bucket and url.bucket ~= '')
if url.path then if url.path then
assert(url.path ~= "") assert(url.path ~= '')
return false return false
end end
return true return true
@ -83,20 +83,20 @@ s3_columns.size = {
render = function(entry, conf) render = function(entry, conf)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
if not meta or not meta.size then if not meta or not meta.size then
return "" return ''
elseif meta.size >= 1e9 then elseif meta.size >= 1e9 then
return string.format("%.1fG", meta.size / 1e9) return string.format('%.1fG', meta.size / 1e9)
elseif meta.size >= 1e6 then elseif meta.size >= 1e6 then
return string.format("%.1fM", meta.size / 1e6) return string.format('%.1fM', meta.size / 1e6)
elseif meta.size >= 1e3 then elseif meta.size >= 1e3 then
return string.format("%.1fk", meta.size / 1e3) return string.format('%.1fk', meta.size / 1e3)
else else
return string.format("%d", meta.size) return string.format('%d', meta.size)
end end
end, end,
parse = function(line, conf) parse = function(line, conf)
return line:match("^(%d+%S*)%s+(.*)$") return line:match('^(%d+%S*)%s+(.*)$')
end, end,
get_sort_value = function(entry) get_sort_value = function(entry)
@ -113,21 +113,21 @@ s3_columns.birthtime = {
render = function(entry, conf) render = function(entry, conf)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
if not meta or not meta.date then if not meta or not meta.date then
return "" return ''
else else
return meta.date return meta.date
end end
end, end,
parse = function(line, conf) parse = function(line, conf)
return line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$") return line:match('^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$')
end, end,
get_sort_value = function(entry) get_sort_value = function(entry)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
if meta and meta.date then if meta and meta.date then
local year, month, day, hour, min, sec = local year, month, day, hour, min, sec =
meta.date:match("^(%d+)%-(%d+)%-(%d+)%s(%d+):(%d+):(%d+)$") meta.date:match('^(%d+)%-(%d+)%-(%d+)%s(%d+):(%d+):(%d+)$')
local time = local time =
os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec }) os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec })
return time return time
@ -148,9 +148,9 @@ end
M.get_parent = function(bufname) M.get_parent = function(bufname)
local res = M.parse_url(bufname) local res = M.parse_url(bufname)
if res.path then if res.path then
assert(res.path ~= "") assert(res.path ~= '')
local path = pathutil.parent(res.path) local path = pathutil.parent(res.path)
res.path = path ~= "" and path or nil res.path = path ~= '' and path or nil
else else
res.bucket = nil res.bucket = nil
end end
@ -168,8 +168,8 @@ end
---@param column_defs string[] ---@param column_defs string[]
---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) ---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
M.list = function(url, column_defs, callback) M.list = function(url, column_defs, callback)
if vim.fn.executable("aws") ~= 1 then if vim.fn.executable('aws') ~= 1 then
callback("`aws` is not executable. Can you run `aws s3 ls`?") callback('`aws` is not executable. Can you run `aws s3 ls`?')
return return
end end
@ -187,16 +187,16 @@ end
---@param action oil.Action ---@param action oil.Action
---@return string ---@return string
M.render_action = function(action) M.render_action = function(action)
local is_folder = action.entry_type == "directory" local is_folder = action.entry_type == 'directory'
if action.type == "create" then if action.type == 'create' then
local res = M.parse_url(action.url) local res = M.parse_url(action.url)
local extra = is_bucket(res) and "BUCKET " or "" local extra = is_bucket(res) and 'BUCKET ' or ''
return string.format("CREATE %s%s", extra, url_to_s3(res, is_folder)) return string.format('CREATE %s%s', extra, url_to_s3(res, is_folder))
elseif action.type == "delete" then elseif action.type == 'delete' then
local res = M.parse_url(action.url) local res = M.parse_url(action.url)
local extra = is_bucket(res) and "BUCKET " or "" local extra = is_bucket(res) and 'BUCKET ' or ''
return string.format("DELETE %s%s", extra, url_to_s3(res, is_folder)) return string.format('DELETE %s%s', extra, url_to_s3(res, is_folder))
elseif action.type == "move" or action.type == "copy" then elseif action.type == 'move' or action.type == 'copy' then
local src = action.src_url local src = action.src_url
local dest = action.dest_url local dest = action.dest_url
if config.get_adapter_by_scheme(src) ~= M then if config.get_adapter_by_scheme(src) ~= M then
@ -210,7 +210,7 @@ M.render_action = function(action)
dest = files.to_short_os_path(path, action.entry_type) dest = files.to_short_os_path(path, action.entry_type)
src = url_to_s3(M.parse_url(src), is_folder) src = url_to_s3(M.parse_url(src), is_folder)
end end
return string.format(" %s %s -> %s", action.type:upper(), src, dest) return string.format(' %s %s -> %s', action.type:upper(), src, dest)
else else
error(string.format("Bad action type: '%s'", action.type)) error(string.format("Bad action type: '%s'", action.type))
end end
@ -219,30 +219,30 @@ end
---@param action oil.Action ---@param action oil.Action
---@param cb fun(err: nil|string) ---@param cb fun(err: nil|string)
M.perform_action = function(action, cb) M.perform_action = function(action, cb)
local is_folder = action.entry_type == "directory" local is_folder = action.entry_type == 'directory'
if action.type == "create" then if action.type == 'create' then
local res = M.parse_url(action.url) local res = M.parse_url(action.url)
local bucket = is_bucket(res) local bucket = is_bucket(res)
if action.entry_type == "directory" and bucket then if action.entry_type == 'directory' and bucket then
s3fs.mb(url_to_s3(res, true), cb) s3fs.mb(url_to_s3(res, true), cb)
elseif action.entry_type == "directory" or action.entry_type == "file" then elseif action.entry_type == 'directory' or action.entry_type == 'file' then
s3fs.touch(url_to_s3(res, is_folder), cb) s3fs.touch(url_to_s3(res, is_folder), cb)
else else
cb(string.format("Bad entry type on s3 create action: %s", action.entry_type)) cb(string.format('Bad entry type on s3 create action: %s', action.entry_type))
end end
elseif action.type == "delete" then elseif action.type == 'delete' then
local res = M.parse_url(action.url) local res = M.parse_url(action.url)
local bucket = is_bucket(res) local bucket = is_bucket(res)
if action.entry_type == "directory" and bucket then if action.entry_type == 'directory' and bucket then
s3fs.rb(url_to_s3(res, true), cb) s3fs.rb(url_to_s3(res, true), cb)
elseif action.entry_type == "directory" or action.entry_type == "file" then elseif action.entry_type == 'directory' or action.entry_type == 'file' then
s3fs.rm(url_to_s3(res, is_folder), is_folder, cb) s3fs.rm(url_to_s3(res, is_folder), is_folder, cb)
else else
cb(string.format("Bad entry type on s3 delete action: %s", action.entry_type)) cb(string.format('Bad entry type on s3 delete action: %s', action.entry_type))
end end
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))
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 if
@ -250,7 +250,7 @@ M.perform_action = function(action, cb)
then then
cb( cb(
string.format( string.format(
"We should never attempt to move from the %s adapter to the %s adapter.", 'We should never attempt to move from the %s adapter to the %s adapter.',
src_adapter.name, src_adapter.name,
dest_adapter.name dest_adapter.name
) )
@ -276,7 +276,7 @@ M.perform_action = function(action, cb)
assert(dest) assert(dest)
s3fs.mv(src, dest, is_folder, cb) s3fs.mv(src, dest, is_folder, cb)
elseif action.type == "copy" then elseif action.type == 'copy' 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))
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 if
@ -284,7 +284,7 @@ M.perform_action = function(action, cb)
then then
cb( cb(
string.format( string.format(
"We should never attempt to copy from the %s adapter to the %s adapter.", 'We should never attempt to copy from the %s adapter to the %s adapter.',
src_adapter.name, src_adapter.name,
dest_adapter.name dest_adapter.name
) )
@ -311,11 +311,11 @@ M.perform_action = function(action, cb)
s3fs.cp(src, dest, is_folder, cb) s3fs.cp(src, dest, is_folder, cb)
else else
cb(string.format("Bad action type: %s", action.type)) cb(string.format('Bad action type: %s', action.type))
end end
end end
M.supported_cross_adapter_actions = { files = "move" } M.supported_cross_adapter_actions = { files = 'move' }
---@param bufnr integer ---@param bufnr integer
M.read_file = function(bufnr) M.read_file = function(bufnr)
@ -323,11 +323,11 @@ M.read_file = function(bufnr)
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local url = M.parse_url(bufname) local url = M.parse_url(bufname)
local basename = pathutil.basename(bufname) local basename = pathutil.basename(bufname)
local cache_dir = vim.fn.stdpath("cache") local cache_dir = vim.fn.stdpath('cache')
assert(type(cache_dir) == "string") assert(type(cache_dir) == 'string')
local tmpdir = fs.join(cache_dir, "oil") local tmpdir = fs.join(cache_dir, 'oil')
fs.mkdirp(tmpdir) fs.mkdirp(tmpdir)
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "s3_XXXXXX")) local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, 's3_XXXXXX'))
if fd then if fd then
vim.loop.fs_close(fd) vim.loop.fs_close(fd)
end end
@ -336,9 +336,9 @@ M.read_file = function(bufnr)
s3fs.cp(url_to_s3(url, false), tmpfile, false, function(err) s3fs.cp(url_to_s3(url, false), tmpfile, false, function(err)
loading.set_loading(bufnr, false) loading.set_loading(bufnr, false)
vim.bo[bufnr].modifiable = true vim.bo[bufnr].modifiable = true
vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { silent = true } }) vim.cmd.doautocmd({ args = { 'BufReadPre', bufname }, mods = { silent = true } })
if err then if err then
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, vim.split(err, "\n")) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, vim.split(err, '\n'))
else else
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {}) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {})
vim.api.nvim_buf_call(bufnr, function() vim.api.nvim_buf_call(bufnr, function()
@ -352,7 +352,7 @@ M.read_file = function(bufnr)
if filetype then if filetype then
vim.bo[bufnr].filetype = filetype vim.bo[bufnr].filetype = filetype
end end
vim.cmd.doautocmd({ args = { "BufReadPost", bufname }, mods = { silent = true } }) vim.cmd.doautocmd({ args = { 'BufReadPost', bufname }, mods = { silent = true } })
vim.api.nvim_buf_delete(tmp_bufnr, { force = true }) vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
end) end)
end end
@ -361,14 +361,14 @@ end
M.write_file = function(bufnr) M.write_file = function(bufnr)
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local url = M.parse_url(bufname) local url = M.parse_url(bufname)
local cache_dir = vim.fn.stdpath("cache") local cache_dir = vim.fn.stdpath('cache')
assert(type(cache_dir) == "string") assert(type(cache_dir) == 'string')
local tmpdir = fs.join(cache_dir, "oil") local tmpdir = fs.join(cache_dir, 'oil')
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "s3_XXXXXXXX")) local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, 's3_XXXXXXXX'))
if fd then if fd then
vim.loop.fs_close(fd) vim.loop.fs_close(fd)
end end
vim.cmd.doautocmd({ args = { "BufWritePre", bufname }, mods = { silent = true } }) vim.cmd.doautocmd({ args = { 'BufWritePre', bufname }, mods = { silent = true } })
vim.bo[bufnr].modifiable = false vim.bo[bufnr].modifiable = false
vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } }) vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } })
local tmp_bufnr = vim.fn.bufadd(tmpfile) local tmp_bufnr = vim.fn.bufadd(tmpfile)
@ -376,10 +376,10 @@ M.write_file = function(bufnr)
s3fs.cp(tmpfile, url_to_s3(url, false), false, function(err) s3fs.cp(tmpfile, url_to_s3(url, false), false, function(err)
vim.bo[bufnr].modifiable = true vim.bo[bufnr].modifiable = true
if err then if err then
vim.notify(string.format("Error writing file: %s", err), vim.log.levels.ERROR) vim.notify(string.format('Error writing file: %s', err), vim.log.levels.ERROR)
else else
vim.bo[bufnr].modified = false vim.bo[bufnr].modified = false
vim.cmd.doautocmd({ args = { "BufWritePost", bufname }, mods = { silent = true } }) vim.cmd.doautocmd({ args = { 'BufWritePost', bufname }, mods = { silent = true } })
end end
vim.loop.fs_unlink(tmpfile) vim.loop.fs_unlink(tmpfile)
vim.api.nvim_buf_delete(tmp_bufnr, { force = true }) vim.api.nvim_buf_delete(tmp_bufnr, { force = true })

View file

@ -1,8 +1,8 @@
local cache = require("oil.cache") local cache = require('oil.cache')
local config = require("oil.config") local config = require('oil.config')
local constants = require("oil.constants") local constants = require('oil.constants')
local shell = require("oil.shell") local shell = require('oil.shell')
local util = require("oil.util") local util = require('oil.util')
local M = {} local M = {}
@ -13,11 +13,11 @@ local FIELD_META = constants.FIELD_META
---@return oil.EntryType ---@return oil.EntryType
---@return table Metadata for entry ---@return table Metadata for entry
local function parse_ls_line_bucket(line) local function parse_ls_line_bucket(line)
local date, name = line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$") local date, name = line:match('^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$')
if not date or not name then if not date or not name then
error(string.format("Could not parse '%s'", line)) error(string.format("Could not parse '%s'", line))
end end
local type = "directory" local type = 'directory'
local meta = { date = date } local meta = { date = date }
return name, type, meta return name, type, meta
end end
@ -27,18 +27,18 @@ end
---@return oil.EntryType ---@return oil.EntryType
---@return table Metadata for entry ---@return table Metadata for entry
local function parse_ls_line_file(line) local function parse_ls_line_file(line)
local name = line:match("^%s+PRE%s+(.*)/$") local name = line:match('^%s+PRE%s+(.*)/$')
local type = "directory" local type = 'directory'
local meta = {} local meta = {}
if name then if name then
return name, type, meta return name, type, meta
end end
local date, size local date, size
date, size, name = line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(%d+)%s+(.*)$") date, size, name = line:match('^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(%d+)%s+(.*)$')
if not name then if not name then
error(string.format("Could not parse '%s'", line)) error(string.format("Could not parse '%s'", line))
end end
type = "file" type = 'file'
meta = { date = date, size = tonumber(size) } meta = { date = date, size = tonumber(size) }
return name, type, meta return name, type, meta
end end
@ -46,7 +46,7 @@ end
---@param cmd string[] cmd and flags ---@param cmd string[] cmd and flags
---@return string[] Shell command to run ---@return string[] Shell command to run
local function create_s3_command(cmd) local function create_s3_command(cmd)
local full_cmd = vim.list_extend({ "aws", "s3" }, cmd) local full_cmd = vim.list_extend({ 'aws', 's3' }, cmd)
return vim.list_extend(full_cmd, config.extra_s3_args) return vim.list_extend(full_cmd, config.extra_s3_args)
end end
@ -54,7 +54,7 @@ end
---@param path string ---@param path string
---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) ---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
function M.list_dir(url, path, callback) function M.list_dir(url, path, callback)
local cmd = create_s3_command({ "ls", path, "--color=off", "--no-cli-pager" }) local cmd = create_s3_command({ 'ls', path, '--color=off', '--no-cli-pager' })
shell.run(cmd, function(err, lines) shell.run(cmd, function(err, lines)
if err then if err then
return callback(err) return callback(err)
@ -63,13 +63,13 @@ function M.list_dir(url, path, callback)
local cache_entries = {} local cache_entries = {}
local url_path, _ local url_path, _
_, url_path = util.parse_url(url) _, url_path = util.parse_url(url)
local is_top_level = url_path == nil or url_path:match("/") == nil 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 local parse_ls_line = is_top_level and parse_ls_line_bucket or parse_ls_line_file
for _, line in ipairs(lines) do for _, line in ipairs(lines) do
if line ~= "" then if line ~= '' then
local name, type, meta = parse_ls_line(line) local name, type, meta = parse_ls_line(line)
-- in s3 '-' can be used to create an "empty folder" -- in s3 '-' can be used to create an "empty folder"
if name ~= "-" then if name ~= '-' then
local cache_entry = cache.create_entry(url, name, type) local cache_entry = cache.create_entry(url, name, type)
table.insert(cache_entries, cache_entry) table.insert(cache_entries, cache_entry)
cache_entry[FIELD_META] = meta cache_entry[FIELD_META] = meta
@ -85,8 +85,8 @@ end
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function M.touch(path, callback) function M.touch(path, callback)
-- here "-" means that we copy from stdin -- here "-" means that we copy from stdin
local cmd = create_s3_command({ "cp", "-", path }) local cmd = create_s3_command({ 'cp', '-', path })
shell.run(cmd, { stdin = "null" }, callback) shell.run(cmd, { stdin = 'null' }, callback)
end end
--- Remove files --- Remove files
@ -94,9 +94,9 @@ end
---@param is_folder boolean ---@param is_folder boolean
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function M.rm(path, is_folder, callback) function M.rm(path, is_folder, callback)
local main_cmd = { "rm", path } local main_cmd = { 'rm', path }
if is_folder then if is_folder then
table.insert(main_cmd, "--recursive") table.insert(main_cmd, '--recursive')
end end
local cmd = create_s3_command(main_cmd) local cmd = create_s3_command(main_cmd)
shell.run(cmd, callback) shell.run(cmd, callback)
@ -106,7 +106,7 @@ end
---@param bucket string ---@param bucket string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function M.rb(bucket, callback) function M.rb(bucket, callback)
local cmd = create_s3_command({ "rb", bucket }) local cmd = create_s3_command({ 'rb', bucket })
shell.run(cmd, callback) shell.run(cmd, callback)
end end
@ -114,7 +114,7 @@ end
---@param bucket string ---@param bucket string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function M.mb(bucket, callback) function M.mb(bucket, callback)
local cmd = create_s3_command({ "mb", bucket }) local cmd = create_s3_command({ 'mb', bucket })
shell.run(cmd, callback) shell.run(cmd, callback)
end end
@ -124,9 +124,9 @@ end
---@param is_folder boolean ---@param is_folder boolean
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function M.mv(src, dest, is_folder, callback) function M.mv(src, dest, is_folder, callback)
local main_cmd = { "mv", src, dest } local main_cmd = { 'mv', src, dest }
if is_folder then if is_folder then
table.insert(main_cmd, "--recursive") table.insert(main_cmd, '--recursive')
end end
local cmd = create_s3_command(main_cmd) local cmd = create_s3_command(main_cmd)
shell.run(cmd, callback) shell.run(cmd, callback)
@ -138,9 +138,9 @@ end
---@param is_folder boolean ---@param is_folder boolean
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function M.cp(src, dest, is_folder, callback) function M.cp(src, dest, is_folder, callback)
local main_cmd = { "cp", src, dest } local main_cmd = { 'cp', src, dest }
if is_folder then if is_folder then
table.insert(main_cmd, "--recursive") table.insert(main_cmd, '--recursive')
end end
local cmd = create_s3_command(main_cmd) local cmd = create_s3_command(main_cmd)
shell.run(cmd, callback) shell.run(cmd, callback)

View file

@ -1,13 +1,13 @@
local config = require("oil.config") local config = require('oil.config')
local constants = require("oil.constants") local constants = require('oil.constants')
local files = require("oil.adapters.files") local files = require('oil.adapters.files')
local fs = require("oil.fs") local fs = require('oil.fs')
local loading = require("oil.loading") local loading = require('oil.loading')
local pathutil = require("oil.pathutil") local pathutil = require('oil.pathutil')
local permissions = require("oil.adapters.files.permissions") local permissions = require('oil.adapters.files.permissions')
local shell = require("oil.shell") local shell = require('oil.shell')
local sshfs = require("oil.adapters.ssh.sshfs") local sshfs = require('oil.adapters.ssh.sshfs')
local util = require("oil.util") local util = require('oil.util')
local M = {} local M = {}
local FIELD_NAME = constants.FIELD_NAME local FIELD_NAME = constants.FIELD_NAME
@ -22,7 +22,7 @@ local FIELD_META = constants.FIELD_META
---@param args string[] ---@param args string[]
local function scp(args, ...) local function scp(args, ...)
local cmd = vim.list_extend({ "scp", "-C" }, config.extra_scp_args) local cmd = vim.list_extend({ 'scp', '-C' }, config.extra_scp_args)
vim.list_extend(cmd, args) vim.list_extend(cmd, args)
shell.run(cmd, ...) shell.run(cmd, ...)
end end
@ -33,21 +33,21 @@ M.parse_url = function(oil_url)
local scheme, url = util.parse_url(oil_url) local scheme, url = util.parse_url(oil_url)
assert(scheme and url, string.format("Malformed input url '%s'", oil_url)) assert(scheme and url, string.format("Malformed input url '%s'", oil_url))
local ret = { scheme = scheme } local ret = { scheme = scheme }
local username, rem = url:match("^([^@%s]+)@(.*)$") local username, rem = url:match('^([^@%s]+)@(.*)$')
ret.user = username ret.user = username
url = rem or url url = rem or url
local host, port, path = url:match("^([^:]+):(%d+)/(.*)$") local host, port, path = url:match('^([^:]+):(%d+)/(.*)$')
if host then if host then
ret.host = host ret.host = host
ret.port = tonumber(port) ret.port = tonumber(port)
ret.path = path ret.path = path
else else
host, path = url:match("^([^/]+)/(.*)$") host, path = url:match('^([^/]+)/(.*)$')
ret.host = host ret.host = host
ret.path = path ret.path = path
end end
if not ret.host or not ret.path then if not ret.host or not ret.path then
error(string.format("Malformed SSH url: %s", oil_url)) error(string.format('Malformed SSH url: %s', oil_url))
end end
---@cast ret oil.sshUrl ---@cast ret oil.sshUrl
@ -60,33 +60,33 @@ local function url_to_str(url)
local pieces = { url.scheme } local pieces = { url.scheme }
if url.user then if url.user then
table.insert(pieces, url.user) table.insert(pieces, url.user)
table.insert(pieces, "@") table.insert(pieces, '@')
end end
table.insert(pieces, url.host) table.insert(pieces, url.host)
if url.port then if url.port then
table.insert(pieces, string.format(":%d", url.port)) table.insert(pieces, string.format(':%d', url.port))
end end
table.insert(pieces, "/") table.insert(pieces, '/')
table.insert(pieces, url.path) table.insert(pieces, url.path)
return table.concat(pieces, "") return table.concat(pieces, '')
end end
---@param url oil.sshUrl ---@param url oil.sshUrl
---@return string ---@return string
local function url_to_scp(url) local function url_to_scp(url)
local pieces = { "scp://" } local pieces = { 'scp://' }
if url.user then if url.user then
table.insert(pieces, url.user) table.insert(pieces, url.user)
table.insert(pieces, "@") table.insert(pieces, '@')
end end
table.insert(pieces, url.host) table.insert(pieces, url.host)
if url.port then if url.port then
table.insert(pieces, string.format(":%d", url.port)) table.insert(pieces, string.format(':%d', url.port))
end end
table.insert(pieces, "/") table.insert(pieces, '/')
local escaped_path = util.url_escape(url.path) local escaped_path = util.url_escape(url.path)
table.insert(pieces, escaped_path) table.insert(pieces, escaped_path)
return table.concat(pieces, "") return table.concat(pieces, '')
end end
---@param url1 oil.sshUrl ---@param url1 oil.sshUrl
@ -103,7 +103,7 @@ local _connections = {}
local function get_connection(url, allow_retry) local function get_connection(url, allow_retry)
local res = M.parse_url(url) local res = M.parse_url(url)
res.scheme = config.adapter_to_scheme.ssh res.scheme = config.adapter_to_scheme.ssh
res.path = "" res.path = ''
local key = url_to_str(res) local key = url_to_str(res)
local conn = _connections[key] local conn = _connections[key]
if not conn or (allow_retry and conn:get_connection_error()) then if not conn or (allow_retry and conn:get_connection_error()) then
@ -137,7 +137,7 @@ ssh_columns.permissions = {
end, end,
render_action = function(action) render_action = function(action)
return string.format("CHMOD %s %s", permissions.mode_to_octal_str(action.value), action.url) return string.format('CHMOD %s %s', permissions.mode_to_octal_str(action.value), action.url)
end, end,
perform_action = function(action, callback) perform_action = function(action, callback)
@ -151,20 +151,20 @@ ssh_columns.size = {
render = function(entry, conf) render = function(entry, conf)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
if not meta or not meta.size then if not meta or not meta.size then
return "" return ''
elseif meta.size >= 1e9 then elseif meta.size >= 1e9 then
return string.format("%.1fG", meta.size / 1e9) return string.format('%.1fG', meta.size / 1e9)
elseif meta.size >= 1e6 then elseif meta.size >= 1e6 then
return string.format("%.1fM", meta.size / 1e6) return string.format('%.1fM', meta.size / 1e6)
elseif meta.size >= 1e3 then elseif meta.size >= 1e3 then
return string.format("%.1fk", meta.size / 1e3) return string.format('%.1fk', meta.size / 1e3)
else else
return string.format("%d", meta.size) return string.format('%d', meta.size)
end end
end, end,
parse = function(line, conf) parse = function(line, conf)
return line:match("^(%d+%S*)%s+(.*)$") return line:match('^(%d+%S*)%s+(.*)$')
end, end,
get_sort_value = function(entry) get_sort_value = function(entry)
@ -206,13 +206,13 @@ M.normalize_url = function(url, callback)
local conn = get_connection(url, true) local conn = get_connection(url, true)
local path = res.path local path = res.path
if path == "" then if path == '' then
path = "." path = '.'
end end
conn:realpath(path, function(err, abspath) conn:realpath(path, function(err, abspath)
if err then if err then
vim.notify(string.format("Error normalizing url %s: %s", url, err), vim.log.levels.WARN) vim.notify(string.format('Error normalizing url %s: %s', url, err), vim.log.levels.WARN)
callback(url) callback(url)
else else
res.path = abspath res.path = abspath
@ -259,15 +259,15 @@ end
---@param action oil.Action ---@param action oil.Action
---@return string ---@return string
M.render_action = function(action) M.render_action = function(action)
if action.type == "create" then if action.type == 'create' then
local ret = string.format("CREATE %s", action.url) local ret = string.format('CREATE %s', action.url)
if action.link then if action.link then
ret = ret .. " -> " .. action.link ret = ret .. ' -> ' .. action.link
end end
return ret return ret
elseif action.type == "delete" then elseif action.type == 'delete' then
return string.format("DELETE %s", action.url) return string.format('DELETE %s', action.url)
elseif action.type == "move" or action.type == "copy" then elseif action.type == 'move' or action.type == 'copy' then
local src = action.src_url local src = action.src_url
local dest = action.dest_url local dest = action.dest_url
if config.get_adapter_by_scheme(src) == M then if config.get_adapter_by_scheme(src) == M then
@ -279,7 +279,7 @@ M.render_action = function(action)
assert(path) assert(path)
src = files.to_short_os_path(path, action.entry_type) src = files.to_short_os_path(path, action.entry_type)
end end
return string.format(" %s %s -> %s", action.type:upper(), src, dest) return string.format(' %s %s -> %s', action.type:upper(), src, dest)
else else
error(string.format("Bad action type: '%s'", action.type)) error(string.format("Bad action type: '%s'", action.type))
end end
@ -288,21 +288,21 @@ end
---@param action oil.Action ---@param action oil.Action
---@param cb fun(err: nil|string) ---@param cb fun(err: nil|string)
M.perform_action = function(action, cb) M.perform_action = function(action, cb)
if action.type == "create" then if action.type == 'create' then
local res = M.parse_url(action.url) local res = M.parse_url(action.url)
local conn = get_connection(action.url) local conn = get_connection(action.url)
if action.entry_type == "directory" then if action.entry_type == 'directory' then
conn:mkdir(res.path, cb) conn:mkdir(res.path, cb)
elseif action.entry_type == "link" and action.link then elseif action.entry_type == 'link' and action.link then
conn:mklink(res.path, action.link, cb) conn:mklink(res.path, action.link, cb)
else else
conn:touch(res.path, cb) conn:touch(res.path, cb)
end end
elseif action.type == "delete" then elseif action.type == 'delete' then
local res = M.parse_url(action.url) local res = M.parse_url(action.url)
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 = assert(config.get_adapter_by_scheme(action.src_url)) local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) 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
@ -311,7 +311,7 @@ M.perform_action = function(action, cb)
local src_conn = get_connection(action.src_url) local src_conn = get_connection(action.src_url)
local dest_conn = get_connection(action.dest_url) local dest_conn = get_connection(action.dest_url)
if src_conn ~= dest_conn then if src_conn ~= dest_conn then
scp({ "-r", url_to_scp(src_res), url_to_scp(dest_res) }, function(err) scp({ '-r', url_to_scp(src_res), url_to_scp(dest_res) }, function(err)
if err then if err then
return cb(err) return cb(err)
end end
@ -321,16 +321,16 @@ M.perform_action = function(action, cb)
src_conn:mv(src_res.path, dest_res.path, cb) src_conn:mv(src_res.path, dest_res.path, cb)
end end
else else
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 = assert(config.get_adapter_by_scheme(action.src_url)) local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter == M and dest_adapter == M then if src_adapter == M and dest_adapter == M then
local src_res = M.parse_url(action.src_url) local src_res = M.parse_url(action.src_url)
local dest_res = M.parse_url(action.dest_url) local dest_res = M.parse_url(action.dest_url)
if not url_hosts_equal(src_res, dest_res) then if not url_hosts_equal(src_res, dest_res) then
scp({ "-r", url_to_scp(src_res), url_to_scp(dest_res) }, cb) scp({ '-r', url_to_scp(src_res), url_to_scp(dest_res) }, cb)
else else
local src_conn = get_connection(action.src_url) local src_conn = get_connection(action.src_url)
src_conn:cp(src_res.path, dest_res.path, cb) src_conn:cp(src_res.path, dest_res.path, cb)
@ -349,14 +349,14 @@ M.perform_action = function(action, cb)
src_arg = fs.posix_to_os_path(path) src_arg = fs.posix_to_os_path(path)
dest_arg = url_to_scp(M.parse_url(action.dest_url)) dest_arg = url_to_scp(M.parse_url(action.dest_url))
end end
scp({ "-r", src_arg, dest_arg }, cb) scp({ '-r', src_arg, dest_arg }, cb)
end end
else else
cb(string.format("Bad action type: %s", action.type)) cb(string.format('Bad action type: %s', action.type))
end end
end end
M.supported_cross_adapter_actions = { files = "copy" } M.supported_cross_adapter_actions = { files = 'copy' }
---@param bufnr integer ---@param bufnr integer
M.read_file = function(bufnr) M.read_file = function(bufnr)
@ -365,11 +365,11 @@ M.read_file = function(bufnr)
local url = M.parse_url(bufname) local url = M.parse_url(bufname)
local scp_url = url_to_scp(url) local scp_url = url_to_scp(url)
local basename = pathutil.basename(bufname) local basename = pathutil.basename(bufname)
local cache_dir = vim.fn.stdpath("cache") local cache_dir = vim.fn.stdpath('cache')
assert(type(cache_dir) == "string") assert(type(cache_dir) == 'string')
local tmpdir = fs.join(cache_dir, "oil") local tmpdir = fs.join(cache_dir, 'oil')
fs.mkdirp(tmpdir) fs.mkdirp(tmpdir)
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXX")) local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, 'ssh_XXXXXX'))
if fd then if fd then
vim.loop.fs_close(fd) vim.loop.fs_close(fd)
end end
@ -378,9 +378,9 @@ M.read_file = function(bufnr)
scp({ scp_url, tmpfile }, function(err) scp({ scp_url, tmpfile }, function(err)
loading.set_loading(bufnr, false) loading.set_loading(bufnr, false)
vim.bo[bufnr].modifiable = true vim.bo[bufnr].modifiable = true
vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { silent = true } }) vim.cmd.doautocmd({ args = { 'BufReadPre', bufname }, mods = { silent = true } })
if err then if err then
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, vim.split(err, "\n")) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, vim.split(err, '\n'))
else else
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {}) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {})
vim.api.nvim_buf_call(bufnr, function() vim.api.nvim_buf_call(bufnr, function()
@ -394,9 +394,9 @@ M.read_file = function(bufnr)
if filetype then if filetype then
vim.bo[bufnr].filetype = filetype vim.bo[bufnr].filetype = filetype
end end
vim.cmd.doautocmd({ args = { "BufReadPost", bufname }, mods = { silent = true } }) vim.cmd.doautocmd({ args = { 'BufReadPost', bufname }, mods = { silent = true } })
vim.api.nvim_buf_delete(tmp_bufnr, { force = true }) vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
vim.keymap.set("n", "gf", M.goto_file, { buffer = bufnr }) vim.keymap.set('n', 'gf', M.goto_file, { buffer = bufnr })
end) end)
end end
@ -405,14 +405,14 @@ M.write_file = function(bufnr)
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local url = M.parse_url(bufname) local url = M.parse_url(bufname)
local scp_url = url_to_scp(url) local scp_url = url_to_scp(url)
local cache_dir = vim.fn.stdpath("cache") local cache_dir = vim.fn.stdpath('cache')
assert(type(cache_dir) == "string") assert(type(cache_dir) == 'string')
local tmpdir = fs.join(cache_dir, "oil") local tmpdir = fs.join(cache_dir, 'oil')
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXXXX")) local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, 'ssh_XXXXXXXX'))
if fd then if fd then
vim.loop.fs_close(fd) vim.loop.fs_close(fd)
end end
vim.cmd.doautocmd({ args = { "BufWritePre", bufname }, mods = { silent = true } }) vim.cmd.doautocmd({ args = { 'BufWritePre', bufname }, mods = { silent = true } })
vim.bo[bufnr].modifiable = false vim.bo[bufnr].modifiable = false
vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } }) vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } })
local tmp_bufnr = vim.fn.bufadd(tmpfile) local tmp_bufnr = vim.fn.bufadd(tmpfile)
@ -420,10 +420,10 @@ M.write_file = function(bufnr)
scp({ tmpfile, scp_url }, function(err) scp({ tmpfile, scp_url }, function(err)
vim.bo[bufnr].modifiable = true vim.bo[bufnr].modifiable = true
if err then if err then
vim.notify(string.format("Error writing file: %s", err), vim.log.levels.ERROR) vim.notify(string.format('Error writing file: %s', err), vim.log.levels.ERROR)
else else
vim.bo[bufnr].modified = false vim.bo[bufnr].modified = false
vim.cmd.doautocmd({ args = { "BufWritePost", bufname }, mods = { silent = true } }) vim.cmd.doautocmd({ args = { 'BufWritePost', bufname }, mods = { silent = true } })
end end
vim.loop.fs_unlink(tmpfile) vim.loop.fs_unlink(tmpfile)
vim.api.nvim_buf_delete(tmp_bufnr, { force = true }) vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
@ -432,7 +432,7 @@ end
M.goto_file = function() M.goto_file = function()
local url = M.parse_url(vim.api.nvim_buf_get_name(0)) local url = M.parse_url(vim.api.nvim_buf_get_name(0))
local fname = vim.fn.expand("<cfile>") local fname = vim.fn.expand('<cfile>')
local fullpath = fname local fullpath = fname
if not fs.is_absolute(fname) then if not fs.is_absolute(fname) then
local pardir = vim.fs.dirname(url.path) local pardir = vim.fs.dirname(url.path)
@ -459,7 +459,7 @@ M.goto_file = function()
vim.cmd.edit({ args = { url_to_str(url) } }) vim.cmd.edit({ args = { url_to_str(url) } })
return return
end end
for suffix in vim.gsplit(vim.o.suffixesadd, ",", { plain = true, trimempty = true }) do for suffix in vim.gsplit(vim.o.suffixesadd, ',', { plain = true, trimempty = true }) do
local suffixname = basename .. suffix local suffixname = basename .. suffix
if name_map[suffixname] then if name_map[suffixname] then
url.path = fullpath .. suffix url.path = fullpath .. suffix

View file

@ -1,6 +1,6 @@
local config = require("oil.config") local config = require('oil.config')
local layout = require("oil.layout") local layout = require('oil.layout')
local util = require("oil.util") local util = require('oil.util')
---@class (exact) oil.sshCommand ---@class (exact) oil.sshCommand
---@field cmd string|string[] ---@field cmd string|string[]
@ -24,12 +24,12 @@ local function output_extend(agg, output)
local start = #agg local start = #agg
if vim.tbl_isempty(agg) then if vim.tbl_isempty(agg) then
for _, line in ipairs(output) do for _, line in ipairs(output) do
line = line:gsub("\r", "") line = line:gsub('\r', '')
table.insert(agg, line) table.insert(agg, line)
end end
else else
for i, v in ipairs(output) do for i, v in ipairs(output) do
v = v:gsub("\r", "") v = v:gsub('\r', '')
if i == 1 then if i == 1 then
agg[#agg] = agg[#agg] .. v agg[#agg] = agg[#agg] .. v
else else
@ -53,7 +53,7 @@ local function get_last_lines(bufnr, num_lines)
vim.api.nvim_buf_get_lines(bufnr, end_line - need_lines, end_line, false), vim.api.nvim_buf_get_lines(bufnr, end_line - need_lines, end_line, false),
lines lines
) )
while not vim.tbl_isempty(lines) and lines[#lines]:match("^%s*$") do while not vim.tbl_isempty(lines) and lines[#lines]:match('^%s*$') do
table.remove(lines) table.remove(lines)
end end
end_line = end_line - need_lines end_line = end_line - need_lines
@ -66,14 +66,14 @@ end
function SSHConnection.create_ssh_command(url) function SSHConnection.create_ssh_command(url)
local host = url.host local host = url.host
if url.user then if url.user then
host = url.user .. "@" .. host host = url.user .. '@' .. host
end end
local command = { local command = {
"ssh", 'ssh',
host, host,
} }
if url.port then if url.port then
table.insert(command, "-p") table.insert(command, '-p')
table.insert(command, url.port) table.insert(command, url.port)
end end
return command return command
@ -84,8 +84,8 @@ end
function SSHConnection.new(url) function SSHConnection.new(url)
local command = SSHConnection.create_ssh_command(url) local command = SSHConnection.create_ssh_command(url)
vim.list_extend(command, { vim.list_extend(command, {
"/bin/sh", '/bin/sh',
"-c", '-c',
-- HACK: For some reason in my testing if I just have "echo READY" it doesn't appear, but if I echo -- HACK: For some reason in my testing if I just have "echo READY" it doesn't appear, but if I echo
-- anything prior to that, it *will* appear. The first line gets swallowed. -- anything prior to that, it *will* appear. The first line gets swallowed.
"echo '_make_newline_'; echo '===READY==='; exec /bin/sh", "echo '_make_newline_'; echo '===READY==='; exec /bin/sh",
@ -112,7 +112,7 @@ function SSHConnection.new(url)
}) })
end) end)
self.term_id = term_id self.term_id = term_id
vim.api.nvim_chan_send(term_id, string.format("ssh %s\r\n", url.host)) vim.api.nvim_chan_send(term_id, string.format('ssh %s\r\n', url.host))
util.hack_around_termopen_autocmd(mode) util.hack_around_termopen_autocmd(mode)
-- If it takes more than 2 seconds to connect, pop open the terminal -- If it takes more than 2 seconds to connect, pop open the terminal
@ -125,7 +125,7 @@ function SSHConnection.new(url)
local jid = vim.fn.jobstart(command, { local jid = vim.fn.jobstart(command, {
pty = true, -- This is require for interactivity pty = true, -- This is require for interactivity
on_stdout = function(j, output) on_stdout = function(j, output)
pcall(vim.api.nvim_chan_send, self.term_id, table.concat(output, "\r\n")) pcall(vim.api.nvim_chan_send, self.term_id, table.concat(output, '\r\n'))
---@diagnostic disable-next-line: invisible ---@diagnostic disable-next-line: invisible
local new_i_start = output_extend(self._stdout, output) local new_i_start = output_extend(self._stdout, output)
self:_handle_output(new_i_start) self:_handle_output(new_i_start)
@ -134,12 +134,12 @@ function SSHConnection.new(url)
pcall( pcall(
vim.api.nvim_chan_send, vim.api.nvim_chan_send,
self.term_id, self.term_id,
string.format("\r\n[Process exited %d]\r\n", code) string.format('\r\n[Process exited %d]\r\n', code)
) )
-- Defer to allow the deferred terminal output handling to kick in first -- Defer to allow the deferred terminal output handling to kick in first
vim.defer_fn(function() vim.defer_fn(function()
if code == 0 then if code == 0 then
self:_set_connection_error("SSH connection terminated gracefully") self:_set_connection_error('SSH connection terminated gracefully')
else else
self:_set_connection_error( self:_set_connection_error(
'Unknown SSH error\nTo see more, run :lua require("oil.adapters.ssh").open_terminal()' 'Unknown SSH error\nTo see more, run :lua require("oil.adapters.ssh").open_terminal()'
@ -156,23 +156,23 @@ function SSHConnection.new(url)
else else
self.jid = jid self.jid = jid
end end
self:run("id -u", function(err, lines) self:run('id -u', function(err, lines)
if err then if err then
vim.notify(string.format("Error fetching ssh connection user: %s", err), vim.log.levels.WARN) vim.notify(string.format('Error fetching ssh connection user: %s', err), vim.log.levels.WARN)
else else
assert(lines) assert(lines)
self.meta.user = vim.trim(table.concat(lines, "")) self.meta.user = vim.trim(table.concat(lines, ''))
end end
end) end)
self:run("id -G", function(err, lines) self:run('id -G', function(err, lines)
if err then if err then
vim.notify( vim.notify(
string.format("Error fetching ssh connection user groups: %s", err), string.format('Error fetching ssh connection user groups: %s', err),
vim.log.levels.WARN vim.log.levels.WARN
) )
else else
assert(lines) assert(lines)
self.meta.groups = vim.split(table.concat(lines, ""), "%s+", { trimempty = true }) self.meta.groups = vim.split(table.concat(lines, ''), '%s+', { trimempty = true })
end end
end) end)
@ -197,7 +197,7 @@ function SSHConnection:_handle_output(start_i)
if not self.connected then if not self.connected then
for i = start_i, #self._stdout - 1 do for i = start_i, #self._stdout - 1 do
local line = self._stdout[i] local line = self._stdout[i]
if line == "===READY===" then if line == '===READY===' then
if self.term_winid then if self.term_winid then
if vim.api.nvim_win_is_valid(self.term_winid) then if vim.api.nvim_win_is_valid(self.term_winid) then
vim.api.nvim_win_close(self.term_winid, true) vim.api.nvim_win_close(self.term_winid, true)
@ -215,7 +215,7 @@ function SSHConnection:_handle_output(start_i)
for i = start_i, #self._stdout - 1 do for i = start_i, #self._stdout - 1 do
---@type string ---@type string
local line = self._stdout[i] local line = self._stdout[i]
if line:match("^===BEGIN===%s*$") then if line:match('^===BEGIN===%s*$') then
self._stdout = util.tbl_slice(self._stdout, i + 1) self._stdout = util.tbl_slice(self._stdout, i + 1)
self:_handle_output(1) self:_handle_output(1)
return return
@ -223,15 +223,15 @@ function SSHConnection:_handle_output(start_i)
-- We can't be as strict with the matching (^$) because since we're using a pty the stdout and -- We can't be as strict with the matching (^$) because since we're using a pty the stdout and
-- stderr can be interleaved. If the command had an error, the stderr may interfere with a -- stderr can be interleaved. If the command had an error, the stderr may interfere with a
-- clean print of the done line. -- clean print of the done line.
local exit_code = line:match("===DONE%((%d+)%)===") local exit_code = line:match('===DONE%((%d+)%)===')
if exit_code then if exit_code then
local output = util.tbl_slice(self._stdout, 1, i - 1) local output = util.tbl_slice(self._stdout, 1, i - 1)
local cb = self.commands[1].cb local cb = self.commands[1].cb
self._stdout = util.tbl_slice(self._stdout, i + 1) self._stdout = util.tbl_slice(self._stdout, i + 1)
if exit_code == "0" then if exit_code == '0' then
cb(nil, output) cb(nil, output)
else else
cb(exit_code .. ": " .. table.concat(output, "\n"), output) cb(exit_code .. ': ' .. table.concat(output, '\n'), output)
end end
table.remove(self.commands, 1) table.remove(self.commands, 1)
self:_handle_output(1) self:_handle_output(1)
@ -244,16 +244,17 @@ function SSHConnection:_handle_output(start_i)
local function check_last_line() local function check_last_line()
local last_lines = get_last_lines(self.term_bufnr, 1) local last_lines = get_last_lines(self.term_bufnr, 1)
local last_line = last_lines[1] local last_line = last_lines[1]
if last_line:match("^Are you sure you want to continue connecting") then if last_line:match('^Are you sure you want to continue connecting') then
self:open_terminal() self:open_terminal()
elseif last_line:match("Password:%s*$") then -- selene: allow(if_same_then_else)
elseif last_line:match('Password:%s*$') then
self:open_terminal() self:open_terminal()
elseif last_line:match(": Permission denied %(.+%)%.") then elseif last_line:match(': Permission denied %(.+%)%.') then
self:_set_connection_error(last_line:match(": (Permission denied %(.+%).)")) self:_set_connection_error(last_line:match(': (Permission denied %(.+%).)'))
elseif last_line:match("^ssh: .*Connection refused%s*$") then elseif last_line:match('^ssh: .*Connection refused%s*$') then
self:_set_connection_error("Connection refused") self:_set_connection_error('Connection refused')
elseif last_line:match("^Connection to .+ closed by remote host.%s*$") then elseif last_line:match('^Connection to .+ closed by remote host.%s*$') then
self:_set_connection_error("Connection closed by remote host") self:_set_connection_error('Connection closed by remote host')
end end
end end
-- We have to defer this so the terminal buffer has time to update -- We have to defer this so the terminal buffer has time to update
@ -273,12 +274,12 @@ function SSHConnection:open_terminal()
local row = math.floor((total_height - height) / 2) local row = math.floor((total_height - height) / 2)
local col = math.floor((vim.o.columns - width) / 2) local col = math.floor((vim.o.columns - width) / 2)
self.term_winid = vim.api.nvim_open_win(self.term_bufnr, true, { self.term_winid = vim.api.nvim_open_win(self.term_bufnr, true, {
relative = "editor", relative = 'editor',
width = width, width = width,
height = height, height = height,
row = row, row = row,
col = col, col = col,
style = "minimal", style = 'minimal',
border = config.ssh.border, border = config.ssh.border,
}) })
vim.cmd.startinsert() vim.cmd.startinsert()

View file

@ -1,8 +1,8 @@
local SSHConnection = require("oil.adapters.ssh.connection") local SSHConnection = require('oil.adapters.ssh.connection')
local cache = require("oil.cache") local cache = require('oil.cache')
local constants = require("oil.constants") local constants = require('oil.constants')
local permissions = require("oil.adapters.files.permissions") local permissions = require('oil.adapters.files.permissions')
local util = require("oil.util") local util = require('oil.util')
---@class (exact) oil.sshFs ---@class (exact) oil.sshFs
---@field new fun(url: oil.sshUrl): oil.sshFs ---@field new fun(url: oil.sshUrl): oil.sshFs
@ -13,13 +13,13 @@ local FIELD_TYPE = constants.FIELD_TYPE
local FIELD_META = constants.FIELD_META local FIELD_META = constants.FIELD_META
local typechar_map = { local typechar_map = {
l = "link", l = 'link',
d = "directory", d = 'directory',
p = "fifo", p = 'fifo',
s = "socket", s = 'socket',
["-"] = "file", ['-'] = 'file',
c = "file", -- character special file c = 'file', -- character special file
b = "file", -- block special file b = 'file', -- block special file
} }
---@param line string ---@param line string
---@return string Name of entry ---@return string Name of entry
@ -27,11 +27,11 @@ local typechar_map = {
---@return table Metadata for entry ---@return table Metadata for entry
local function parse_ls_line(line) local function parse_ls_line(line)
local typechar, perms, refcount, user, group, rem = local typechar, perms, refcount, user, group, rem =
line:match("^(.)(%S+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(.*)$") line:match('^(.)(%S+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(.*)$')
if not typechar then if not typechar then
error(string.format("Could not parse '%s'", line)) error(string.format("Could not parse '%s'", line))
end end
local type = typechar_map[typechar] or "file" local type = typechar_map[typechar] or 'file'
local meta = { local meta = {
user = user, user = user,
@ -40,26 +40,26 @@ local function parse_ls_line(line)
refcount = tonumber(refcount), refcount = tonumber(refcount),
} }
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 if name == nil then
major, minor, date, name = major, minor, date, name =
rem:match("^(%d+)%s*,%s*(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)") rem:match('^(%d+)%s*,%s*(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)')
end 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 if name == nil then
size, date, name = rem:match("^(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)") size, date, name = rem:match('^(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)')
end end
meta.size = tonumber(size) meta.size = tonumber(size)
end end
meta.iso_modified_date = date meta.iso_modified_date = date
if type == "link" then if type == 'link' then
local link local link
name, link = unpack(vim.split(name, " -> ", { plain = true })) name, link = unpack(vim.split(name, ' -> ', { plain = true }))
if vim.endswith(link, "/") then if vim.endswith(link, '/') then
link = link:sub(1, #link - 1) link = link:sub(1, #link - 1)
end end
meta.link = link meta.link = link
@ -94,7 +94,7 @@ end
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:chmod(value, path, callback) function SSHFS:chmod(value, path, callback)
local octal = permissions.mode_to_octal_str(value) local octal = permissions.mode_to_octal_str(value)
self.conn:run(string.format("chmod %s %s", octal, shellescape(path)), callback) self.conn:run(string.format('chmod %s %s', octal, shellescape(path)), callback)
end end
function SSHFS:open_terminal() function SSHFS:open_terminal()
@ -114,24 +114,24 @@ function SSHFS:realpath(path, callback)
return callback(err) return callback(err)
end end
assert(lines) assert(lines)
local abspath = table.concat(lines, "") local abspath = table.concat(lines, '')
-- If the path was "." then the abspath might be /path/to/., so we need to trim that final '.' -- If the path was "." then the abspath might be /path/to/., so we need to trim that final '.'
if vim.endswith(abspath, ".") then if vim.endswith(abspath, '.') then
abspath = abspath:sub(1, #abspath - 1) abspath = abspath:sub(1, #abspath - 1)
end end
self.conn:run( self.conn:run(
string.format("LC_ALL=C ls -land --color=never %s", shellescape(abspath)), string.format('LC_ALL=C ls -land --color=never %s', shellescape(abspath)),
function(ls_err, ls_lines) function(ls_err, ls_lines)
local type local type
if ls_err then if ls_err then
-- If the file doesn't exist, treat it like a not-yet-existing directory -- If the file doesn't exist, treat it like a not-yet-existing directory
type = "directory" type = 'directory'
else else
assert(ls_lines) assert(ls_lines)
local _ local _
_, type = parse_ls_line(ls_lines[1]) _, type = parse_ls_line(ls_lines[1])
end end
if type == "directory" then if type == 'directory' then
abspath = util.addslash(abspath) abspath = util.addslash(abspath)
end end
callback(nil, abspath) callback(nil, abspath)
@ -146,13 +146,13 @@ local dir_meta = {}
---@param path string ---@param path string
---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) ---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
function SSHFS:list_dir(url, path, callback) function SSHFS:list_dir(url, path, callback)
local path_postfix = "" local path_postfix = ''
if path ~= "" then if path ~= '' then
path_postfix = string.format(" %s", shellescape(path)) path_postfix = string.format(' %s', shellescape(path))
end end
self.conn:run("LC_ALL=C ls -lan --color=never" .. path_postfix, function(err, lines) self.conn:run('LC_ALL=C ls -lan --color=never' .. path_postfix, function(err, lines)
if err then if err then
if err:match("No such file or directory%s*$") then if err:match('No such file or directory%s*$') then
-- If the directory doesn't exist, treat the list as a success. We will be able to traverse -- If the directory doesn't exist, treat the list as a success. We will be able to traverse
-- and edit a not-yet-existing directory. -- and edit a not-yet-existing directory.
return callback() return callback()
@ -165,12 +165,12 @@ function SSHFS:list_dir(url, path, callback)
local entries = {} local entries = {}
local cache_entries = {} local cache_entries = {}
for _, line in ipairs(lines) do for _, line in ipairs(lines) do
if line ~= "" and not line:match("^total") then if line ~= '' and not line:match('^total') then
local name, type, meta = parse_ls_line(line) local name, type, meta = parse_ls_line(line)
if name == "." then if name == '.' then
dir_meta[url] = meta dir_meta[url] = meta
elseif name ~= ".." then elseif name ~= '..' then
if type == "link" then if type == 'link' then
any_links = true any_links = true
end end
local cache_entry = cache.create_entry(url, name, type) local cache_entry = cache.create_entry(url, name, type)
@ -184,19 +184,19 @@ function SSHFS:list_dir(url, path, callback)
-- If there were any soft links, then we need to run another ls command with -L so that we can -- If there were any soft links, then we need to run another ls command with -L so that we can
-- resolve the type of the link target -- resolve the type of the link target
self.conn:run( self.conn:run(
"LC_ALL=C ls -naLl --color=never" .. path_postfix .. " 2> /dev/null", 'LC_ALL=C ls -naLl --color=never' .. path_postfix .. ' 2> /dev/null',
function(link_err, link_lines) function(link_err, link_lines)
-- Ignore exit code 1. That just means one of the links could not be resolved. -- Ignore exit code 1. That just means one of the links could not be resolved.
if link_err and not link_err:match("^1:") then if link_err and not link_err:match('^1:') then
return callback(link_err) return callback(link_err)
end end
assert(link_lines) assert(link_lines)
for _, line in ipairs(link_lines) do for _, line in ipairs(link_lines) do
if line ~= "" and not line:match("^total") then if line ~= '' and not line:match('^total') then
local ok, name, type, meta = pcall(parse_ls_line, line) local ok, name, type, meta = pcall(parse_ls_line, line)
if ok and name ~= "." and name ~= ".." then if ok and name ~= '.' and name ~= '..' then
local cache_entry = entries[name] local cache_entry = entries[name]
if cache_entry[FIELD_TYPE] == "link" then if cache_entry[FIELD_TYPE] == 'link' then
cache_entry[FIELD_META].link_stat = { cache_entry[FIELD_META].link_stat = {
type = type, type = type,
size = meta.size, size = meta.size,
@ -217,40 +217,40 @@ end
---@param path string ---@param path string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:mkdir(path, callback) function SSHFS:mkdir(path, callback)
self.conn:run(string.format("mkdir -p %s", shellescape(path)), callback) self.conn:run(string.format('mkdir -p %s', shellescape(path)), callback)
end end
---@param path string ---@param path string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:touch(path, callback) function SSHFS:touch(path, callback)
self.conn:run(string.format("touch %s", shellescape(path)), callback) self.conn:run(string.format('touch %s', shellescape(path)), callback)
end end
---@param path string ---@param path string
---@param link string ---@param link string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:mklink(path, link, callback) function SSHFS:mklink(path, link, callback)
self.conn:run(string.format("ln -s %s %s", shellescape(link), shellescape(path)), callback) self.conn:run(string.format('ln -s %s %s', shellescape(link), shellescape(path)), callback)
end end
---@param path string ---@param path string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:rm(path, callback) function SSHFS:rm(path, callback)
self.conn:run(string.format("rm -rf %s", shellescape(path)), callback) self.conn:run(string.format('rm -rf %s', shellescape(path)), callback)
end end
---@param src string ---@param src string
---@param dest string ---@param dest string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:mv(src, dest, callback) function SSHFS:mv(src, dest, callback)
self.conn:run(string.format("mv %s %s", shellescape(src), shellescape(dest)), callback) self.conn:run(string.format('mv %s %s', shellescape(src), shellescape(dest)), callback)
end end
---@param src string ---@param src string
---@param dest string ---@param dest string
---@param callback fun(err: nil|string) ---@param callback fun(err: nil|string)
function SSHFS:cp(src, dest, callback) function SSHFS:cp(src, dest, callback)
self.conn:run(string.format("cp -r %s %s", shellescape(src), shellescape(dest)), callback) self.conn:run(string.format('cp -r %s %s', shellescape(src), shellescape(dest)), callback)
end end
function SSHFS:get_dir_meta(url) function SSHFS:get_dir_meta(url)

View file

@ -1,5 +1,5 @@
local cache = require("oil.cache") local cache = require('oil.cache')
local util = require("oil.util") local util = require('oil.util')
local M = {} local M = {}
---@param url string ---@param url string
@ -32,24 +32,24 @@ end
---@param entry_type oil.EntryType ---@param entry_type oil.EntryType
---@return oil.InternalEntry ---@return oil.InternalEntry
M.test_set = function(path, entry_type) M.test_set = function(path, entry_type)
if path == "/" then if path == '/' then
return {} return {}
end end
local parent = vim.fn.fnamemodify(path, ":h") local parent = vim.fn.fnamemodify(path, ':h')
if parent ~= path then if parent ~= path then
M.test_set(parent, "directory") M.test_set(parent, 'directory')
end end
parent = util.addslash(parent) parent = util.addslash(parent)
if not dir_listing[parent] then if not dir_listing[parent] then
dir_listing[parent] = {} dir_listing[parent] = {}
end end
local name = vim.fn.fnamemodify(path, ":t") local name = vim.fn.fnamemodify(path, ':t')
local entry = { local entry = {
name = name, name = name,
entry_type = entry_type, entry_type = entry_type,
} }
table.insert(dir_listing[parent], entry) table.insert(dir_listing[parent], entry)
local parent_url = "oil-test://" .. parent local parent_url = 'oil-test://' .. parent
return cache.create_and_store_entry(parent_url, entry.name, entry.entry_type) return cache.create_and_store_entry(parent_url, entry.name, entry.entry_type)
end end
@ -68,12 +68,12 @@ end
---@param action oil.Action ---@param action oil.Action
---@return string ---@return string
M.render_action = function(action) M.render_action = function(action)
if action.type == "create" or action.type == "delete" then if action.type == 'create' or action.type == 'delete' then
return string.format("%s %s", action.type:upper(), action.url) return string.format('%s %s', action.type:upper(), action.url)
elseif action.type == "move" or action.type == "copy" then elseif action.type == 'move' or action.type == 'copy' then
return string.format(" %s %s -> %s", action.type:upper(), action.src_url, action.dest_url) return string.format(' %s %s -> %s', action.type:upper(), action.src_url, action.dest_url)
else else
error("Bad action type") error('Bad action type')
end end
end end

View file

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

View file

@ -1,11 +1,11 @@
-- Based on the FreeDesktop.org trash specification -- Based on the FreeDesktop.org trash specification
-- https://specifications.freedesktop.org/trash/1.0/ -- https://specifications.freedesktop.org/trash/1.0/
local cache = require("oil.cache") local cache = require('oil.cache')
local config = require("oil.config") local config = require('oil.config')
local constants = require("oil.constants") local constants = require('oil.constants')
local files = require("oil.adapters.files") local files = require('oil.adapters.files')
local fs = require("oil.fs") local fs = require('oil.fs')
local util = require("oil.util") local util = require('oil.util')
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
local FIELD_META = constants.FIELD_META local FIELD_META = constants.FIELD_META
@ -14,8 +14,8 @@ local M = {}
local function ensure_trash_dir(path) local function ensure_trash_dir(path)
local mode = 448 -- 0700 local mode = 448 -- 0700
fs.mkdirp(fs.join(path, "info"), mode) fs.mkdirp(fs.join(path, 'info'), mode)
fs.mkdirp(fs.join(path, "files"), mode) fs.mkdirp(fs.join(path, 'files'), mode)
end end
---Gets the location of the home trash dir, creating it if necessary ---Gets the location of the home trash dir, creating it if necessary
@ -23,9 +23,9 @@ end
local function get_home_trash_dir() local function get_home_trash_dir()
local xdg_home = vim.env.XDG_DATA_HOME local xdg_home = vim.env.XDG_DATA_HOME
if not xdg_home then if not xdg_home then
xdg_home = fs.join(assert(uv.os_homedir()), ".local", "share") xdg_home = fs.join(assert(uv.os_homedir()), '.local', 'share')
end end
local trash_dir = fs.join(xdg_home, "Trash") local trash_dir = fs.join(xdg_home, 'Trash')
ensure_trash_dir(trash_dir) ensure_trash_dir(trash_dir)
return trash_dir return trash_dir
end end
@ -43,13 +43,13 @@ end
local function get_top_trash_dirs(path) local function get_top_trash_dirs(path)
local dirs = {} local dirs = {}
local dev = (uv.fs_lstat(path) or {}).dev local dev = (uv.fs_lstat(path) or {}).dev
local top_trash_dirs = vim.fs.find(".Trash", { upward = true, path = path, limit = math.huge }) local top_trash_dirs = vim.fs.find('.Trash', { upward = true, path = path, limit = math.huge })
for _, top_trash_dir in ipairs(top_trash_dirs) do for _, top_trash_dir in ipairs(top_trash_dirs) do
local stat = uv.fs_lstat(top_trash_dir) local stat = uv.fs_lstat(top_trash_dir)
if stat and not dev then if stat and not dev then
dev = stat.dev dev = stat.dev
end end
if stat and stat.dev == dev and stat.type == "directory" and is_sticky(stat.mode) then if stat and stat.dev == dev and stat.type == 'directory' and is_sticky(stat.mode) then
local trash_dir = fs.join(top_trash_dir, tostring(uv.getuid())) local trash_dir = fs.join(top_trash_dir, tostring(uv.getuid()))
ensure_trash_dir(trash_dir) ensure_trash_dir(trash_dir)
table.insert(dirs, trash_dir) table.insert(dirs, trash_dir)
@ -58,7 +58,7 @@ local function get_top_trash_dirs(path)
-- Also search for the .Trash-$uid -- Also search for the .Trash-$uid
top_trash_dirs = vim.fs.find( top_trash_dirs = vim.fs.find(
string.format(".Trash-%d", uv.getuid()), string.format('.Trash-%d', uv.getuid()),
{ upward = true, path = path, limit = math.huge } { upward = true, path = path, limit = math.huge }
) )
for _, top_trash_dir in ipairs(top_trash_dirs) do for _, top_trash_dir in ipairs(top_trash_dirs) do
@ -91,14 +91,14 @@ local function get_write_trash_dir(path)
return top_trash_dirs[1] return top_trash_dirs[1]
end end
local parent = vim.fn.fnamemodify(path, ":h") local parent = vim.fn.fnamemodify(path, ':h')
local next_parent = vim.fn.fnamemodify(parent, ":h") local next_parent = vim.fn.fnamemodify(parent, ':h')
while parent ~= next_parent and uv.fs_lstat(next_parent).dev == dev do while parent ~= next_parent and uv.fs_lstat(next_parent).dev == dev do
parent = next_parent parent = next_parent
next_parent = vim.fn.fnamemodify(parent, ":h") next_parent = vim.fn.fnamemodify(parent, ':h')
end end
local top_trash = fs.join(parent, string.format(".Trash-%d", uv.getuid())) local top_trash = fs.join(parent, string.format('.Trash-%d', uv.getuid()))
ensure_trash_dir(top_trash) ensure_trash_dir(top_trash)
return top_trash return top_trash
end end
@ -116,7 +116,7 @@ end
M.normalize_url = function(url, callback) M.normalize_url = function(url, callback)
local scheme, path = util.parse_url(url) local scheme, path = util.parse_url(url)
assert(path) assert(path)
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p") local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ':p')
uv.fs_realpath( uv.fs_realpath(
os_path, os_path,
vim.schedule_wrap(function(err, new_os_path) vim.schedule_wrap(function(err, new_os_path)
@ -140,10 +140,10 @@ M.get_entry_path = function(url, entry, cb)
return return
end end
local path = fs.os_to_posix_path(trash_info.trash_file) local path = fs.os_to_posix_path(trash_info.trash_file)
if meta.stat.type == "directory" then if meta.stat.type == 'directory' then
path = util.addslash(path) path = util.addslash(path)
end end
cb("oil://" .. path) cb('oil://' .. path)
end end
---@class oil.TrashInfo ---@class oil.TrashInfo
@ -156,10 +156,10 @@ end
---@param info_file string ---@param info_file string
---@param cb fun(err?: string, info?: oil.TrashInfo) ---@param cb fun(err?: string, info?: oil.TrashInfo)
local function read_trash_info(info_file, cb) local function read_trash_info(info_file, cb)
if not vim.endswith(info_file, ".trashinfo") then if not vim.endswith(info_file, '.trashinfo') then
return cb("File is not .trashinfo") return cb('File is not .trashinfo')
end end
uv.fs_open(info_file, "r", 448, function(err, fd) uv.fs_open(info_file, 'r', 448, function(err, fd)
if err then if err then
return cb(err) return cb(err)
end end
@ -182,32 +182,32 @@ local function read_trash_info(info_file, cb)
local trash_info = { local trash_info = {
info_file = info_file, info_file = info_file,
} }
local lines = vim.split(content, "\r?\n") local lines = vim.split(content, '\r?\n')
if lines[1] ~= "[Trash Info]" then if lines[1] ~= '[Trash Info]' then
return cb("File missing [Trash Info] header") return cb('File missing [Trash Info] header')
end end
local trash_base = vim.fn.fnamemodify(info_file, ":h:h") local trash_base = vim.fn.fnamemodify(info_file, ':h:h')
for _, line in ipairs(lines) do for _, line in ipairs(lines) do
local key, value = unpack(vim.split(line, "=", { plain = true, trimempty = true })) local key, value = unpack(vim.split(line, '=', { plain = true, trimempty = true }))
if key == "Path" and not trash_info.original_path then if key == 'Path' and not trash_info.original_path then
if not vim.startswith(value, "/") then if not vim.startswith(value, '/') then
value = fs.join(trash_base, value) value = fs.join(trash_base, value)
end end
trash_info.original_path = value trash_info.original_path = value
elseif key == "DeletionDate" and not trash_info.deletion_date then elseif key == 'DeletionDate' and not trash_info.deletion_date then
trash_info.deletion_date = vim.fn.strptime("%Y-%m-%dT%H:%M:%S", value) trash_info.deletion_date = vim.fn.strptime('%Y-%m-%dT%H:%M:%S', value)
end end
end end
if not trash_info.original_path or not trash_info.deletion_date then if not trash_info.original_path or not trash_info.deletion_date then
return cb("File missing required fields") return cb('File missing required fields')
end end
local basename = vim.fn.fnamemodify(info_file, ":t:r") local basename = vim.fn.fnamemodify(info_file, ':t:r')
trash_info.trash_file = fs.join(trash_base, "files", basename) trash_info.trash_file = fs.join(trash_base, 'files', basename)
uv.fs_lstat(trash_info.trash_file, function(trash_stat_err, trash_stat) uv.fs_lstat(trash_info.trash_file, function(trash_stat_err, trash_stat)
if trash_stat_err then if trash_stat_err then
cb(".trashinfo file points to non-existant file") cb('.trashinfo file points to non-existant file')
else else
trash_info.stat = trash_stat trash_info.stat = trash_stat
---@cast trash_info oil.TrashInfo ---@cast trash_info oil.TrashInfo
@ -244,14 +244,14 @@ M.list = function(url, column_defs, cb)
-- The first trash dir is a special case; it is in the home directory and we should only show -- The first trash dir is a special case; it is in the home directory and we should only show
-- all entries if we are in the top root path "/" -- all entries if we are in the top root path "/"
if trash_idx == 1 then if trash_idx == 1 then
show_all_files = path == "/" show_all_files = path == '/'
end end
local info_dir = fs.join(trash_dir, "info") local info_dir = fs.join(trash_dir, 'info')
---@diagnostic disable-next-line: param-type-mismatch, discard-returns ---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(info_dir, function(open_err, fd) uv.fs_opendir(info_dir, function(open_err, fd)
if open_err then if open_err then
if open_err:match("^ENOENT: no such file or directory") then if open_err:match('^ENOENT: no such file or directory') then
-- If the directory doesn't exist, treat the list as a success. We will be able to traverse -- If the directory doesn't exist, treat the list as a success. We will be able to traverse
-- and edit a not-yet-existing directory. -- and edit a not-yet-existing directory.
return read_next_trash_dir() return read_next_trash_dir()
@ -286,12 +286,12 @@ M.list = function(url, column_defs, cb)
-- files. -- files.
poll() poll()
else else
local parent = util.addslash(vim.fn.fnamemodify(info.original_path, ":h")) local parent = util.addslash(vim.fn.fnamemodify(info.original_path, ':h'))
if path == parent or show_all_files then if path == parent or show_all_files then
local name = vim.fn.fnamemodify(info.trash_file, ":t") local name = vim.fn.fnamemodify(info.trash_file, ':t')
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local cache_entry = cache.create_entry(url, name, info.stat.type) local cache_entry = cache.create_entry(url, name, info.stat.type)
local display_name = vim.fn.fnamemodify(info.original_path, ":t") local display_name = vim.fn.fnamemodify(info.original_path, ':t')
cache_entry[FIELD_META] = { cache_entry[FIELD_META] = {
stat = info.stat, stat = info.stat,
trash_info = info, trash_info = info,
@ -302,12 +302,12 @@ M.list = function(url, column_defs, cb)
if path ~= parent and (show_all_files or fs.is_subpath(path, parent)) then if path ~= parent and (show_all_files or fs.is_subpath(path, parent)) then
local name = parent:sub(path:len() + 1) local name = parent:sub(path:len() + 1)
local next_par = vim.fs.dirname(name) local next_par = vim.fs.dirname(name)
while next_par ~= "." do while next_par ~= '.' do
name = next_par name = next_par
next_par = vim.fs.dirname(name) next_par = vim.fs.dirname(name)
end end
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local cache_entry = cache.create_entry(url, name, "directory") local cache_entry = cache.create_entry(url, name, 'directory')
cache_entry[FIELD_META] = { cache_entry[FIELD_META] = {
stat = info.stat, stat = info.stat,
@ -348,7 +348,7 @@ local file_columns = {}
local current_year local current_year
-- Make sure we run this import-time effect in the main loop (mostly for tests) -- Make sure we run this import-time effect in the main loop (mostly for tests)
vim.schedule(function() vim.schedule(function()
current_year = vim.fn.strftime("%Y") current_year = vim.fn.strftime('%Y')
end) end)
file_columns.mtime = { file_columns.mtime = {
@ -368,11 +368,11 @@ file_columns.mtime = {
if fmt then if fmt then
ret = vim.fn.strftime(fmt, time) ret = vim.fn.strftime(fmt, time)
else else
local year = vim.fn.strftime("%Y", time) local year = vim.fn.strftime('%Y', time)
if year ~= current_year then if year ~= current_year then
ret = vim.fn.strftime("%b %d %Y", time) ret = vim.fn.strftime('%b %d %Y', time)
else else
ret = vim.fn.strftime("%b %d %H:%M", time) ret = vim.fn.strftime('%b %d %H:%M', time)
end end
end end
return ret return ret
@ -393,11 +393,11 @@ file_columns.mtime = {
local fmt = conf and conf.format local fmt = conf and conf.format
local pattern local pattern
if fmt then if fmt then
pattern = fmt:gsub("%%.", "%%S+") pattern = fmt:gsub('%%.', '%%S+')
else else
pattern = "%S+%s+%d+%s+%d%d:?%d%d" pattern = '%S+%s+%d+%s+%d%d:?%d%d'
end end
return line:match("^(" .. pattern .. ")%s+(.+)$") return line:match('^(' .. pattern .. ')%s+(.+)$')
end, end,
} }
@ -407,25 +407,26 @@ M.get_column = function(name)
return file_columns[name] return file_columns[name]
end end
M.supported_cross_adapter_actions = { files = "move" } M.supported_cross_adapter_actions = { files = 'move' }
---@param action oil.Action ---@param action oil.Action
---@return boolean ---@return boolean
M.filter_action = function(action) M.filter_action = function(action)
if action.type == "create" then if action.type == 'create' then
return false return false
elseif action.type == "delete" then elseif action.type == 'delete' then
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]
return meta ~= nil and meta.trash_info ~= nil return meta ~= nil and meta.trash_info ~= nil
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))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
return src_adapter.name == "files" or dest_adapter.name == "files" return src_adapter.name == 'files' or dest_adapter.name == 'files'
elseif action.type == "copy" then -- selene: allow(if_same_then_else)
elseif action.type == 'copy' 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))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
return src_adapter.name == "files" or dest_adapter.name == "files" return src_adapter.name == 'files' or dest_adapter.name == 'files'
else else
error(string.format("Bad action type '%s'", action.type)) error(string.format("Bad action type '%s'", action.type))
end end
@ -434,7 +435,7 @@ end
---@param err oil.ParseError ---@param err oil.ParseError
---@return boolean ---@return boolean
M.filter_error = function(err) M.filter_error = function(err)
if err.message == "Duplicate filename" then if err.message == 'Duplicate filename' then
return false return false
end end
return true return true
@ -443,44 +444,44 @@ end
---@param action oil.Action ---@param action oil.Action
---@return string ---@return string
M.render_action = function(action) M.render_action = function(action)
if action.type == "delete" then if action.type == 'delete' then
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 = assert(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
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter.name == "files" then if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url) local _, path = util.parse_url(action.src_url)
assert(path) assert(path)
local short_path = files.to_short_os_path(path, action.entry_type) local short_path = files.to_short_os_path(path, action.entry_type)
return string.format(" TRASH %s", short_path) return string.format(' TRASH %s', short_path)
elseif dest_adapter.name == "files" then elseif dest_adapter.name == 'files' then
local _, path = util.parse_url(action.dest_url) local _, path = util.parse_url(action.dest_url)
assert(path) assert(path)
local short_path = files.to_short_os_path(path, action.entry_type) local short_path = files.to_short_os_path(path, action.entry_type)
return string.format("RESTORE %s", short_path) return string.format('RESTORE %s', short_path)
else else
error("Must be moving files into or out of trash") error('Must be moving files into or out of trash')
end end
elseif action.type == "copy" then elseif action.type == 'copy' 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))
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 src_adapter.name == "files" then if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url) local _, path = util.parse_url(action.src_url)
assert(path) assert(path)
local short_path = files.to_short_os_path(path, action.entry_type) local short_path = files.to_short_os_path(path, action.entry_type)
return string.format(" COPY %s -> TRASH", short_path) return string.format(' COPY %s -> TRASH', short_path)
elseif dest_adapter.name == "files" then elseif dest_adapter.name == 'files' then
local _, path = util.parse_url(action.dest_url) local _, path = util.parse_url(action.dest_url)
assert(path) assert(path)
local short_path = files.to_short_os_path(path, action.entry_type) local short_path = files.to_short_os_path(path, action.entry_type)
return string.format("RESTORE %s", short_path) return string.format('RESTORE %s', short_path)
else else
error("Must be copying files into or out of trash") error('Must be copying files into or out of trash')
end end
else else
error(string.format("Bad action type '%s'", action.type)) error(string.format("Bad action type '%s'", action.type))
@ -490,7 +491,7 @@ end
---@param trash_info oil.TrashInfo ---@param trash_info oil.TrashInfo
---@param cb fun(err?: string) ---@param cb fun(err?: string)
local function purge(trash_info, cb) local function purge(trash_info, cb)
fs.recursive_delete("file", trash_info.info_file, function(err) fs.recursive_delete('file', trash_info.info_file, function(err)
if err then if err then
return cb(err) return cb(err)
end end
@ -505,15 +506,15 @@ end
local function write_info_file(path, info_path, cb) local function write_info_file(path, info_path, cb)
uv.fs_open( uv.fs_open(
info_path, info_path,
"w", 'w',
448, 448,
vim.schedule_wrap(function(err, fd) vim.schedule_wrap(function(err, fd)
if err then if err then
return cb(err) return cb(err)
end end
assert(fd) assert(fd)
local deletion_date = vim.fn.strftime("%Y-%m-%dT%H:%M:%S") local deletion_date = vim.fn.strftime('%Y-%m-%dT%H:%M:%S')
local contents = string.format("[Trash Info]\nPath=%s\nDeletionDate=%s", path, deletion_date) local contents = string.format('[Trash Info]\nPath=%s\nDeletionDate=%s', path, deletion_date)
uv.fs_write(fd, contents, function(write_err) uv.fs_write(fd, contents, function(write_err)
uv.fs_close(fd, function(close_err) uv.fs_close(fd, function(close_err)
cb(write_err or close_err) cb(write_err or close_err)
@ -529,9 +530,9 @@ local function create_trash_info(path, cb)
local trash_dir = get_write_trash_dir(path) local trash_dir = get_write_trash_dir(path)
local basename = vim.fs.basename(path) local basename = vim.fs.basename(path)
local now = os.time() local now = os.time()
local name = string.format("%s-%d.%d", basename, now, math.random(100000, 999999)) local name = string.format('%s-%d.%d', basename, now, math.random(100000, 999999))
local dest_path = fs.join(trash_dir, "files", name) local dest_path = fs.join(trash_dir, 'files', name)
local dest_info = fs.join(trash_dir, "info", name .. ".trashinfo") local dest_info = fs.join(trash_dir, 'info', name .. '.trashinfo')
uv.fs_lstat(path, function(err, stat) uv.fs_lstat(path, function(err, stat)
if err then if err then
return cb(err) return cb(err)
@ -557,19 +558,19 @@ end
---@param action oil.Action ---@param action oil.Action
---@param cb fun(err: nil|string) ---@param cb fun(err: nil|string)
M.perform_action = function(action, cb) M.perform_action = function(action, cb)
if action.type == "delete" then if action.type == 'delete' then
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 = assert(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))
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 src_adapter.name == "files" then if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url) local _, path = util.parse_url(action.src_url)
M.delete_to_trash(assert(path), cb) M.delete_to_trash(assert(path), cb)
elseif dest_adapter.name == "files" then elseif dest_adapter.name == 'files' then
-- Restore -- Restore
local _, dest_path = util.parse_url(action.dest_url) local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path) assert(dest_path)
@ -584,23 +585,23 @@ M.perform_action = function(action, cb)
uv.fs_unlink(trash_info.info_file, cb) uv.fs_unlink(trash_info.info_file, cb)
end) end)
else else
error("Must be moving files into or out of trash") error('Must be moving files into or out of trash')
end end
elseif action.type == "copy" then elseif action.type == 'copy' 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))
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 src_adapter.name == "files" then if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url) local _, path = util.parse_url(action.src_url)
assert(path) assert(path)
create_trash_info(path, function(err, trash_info) create_trash_info(path, function(err, trash_info)
if err then if err then
cb(err) cb(err)
else else
local stat_type = trash_info.stat.type or "unknown" local stat_type = trash_info.stat.type or 'unknown'
fs.recursive_copy(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb)) fs.recursive_copy(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb))
end end
end) end)
elseif dest_adapter.name == "files" then elseif dest_adapter.name == 'files' then
-- Restore -- Restore
local _, dest_path = util.parse_url(action.dest_url) local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path) assert(dest_path)
@ -610,10 +611,10 @@ M.perform_action = function(action, cb)
local trash_info = assert(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')
end end
else else
cb(string.format("Bad action type: %s", action.type)) cb(string.format('Bad action type: %s', action.type))
end end
end end
@ -624,7 +625,7 @@ M.delete_to_trash = function(path, cb)
if err then if err then
cb(err) cb(err)
else else
local stat_type = trash_info.stat.type or "unknown" local stat_type = trash_info.stat.type or 'unknown'
fs.recursive_move(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb)) fs.recursive_move(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb))
end end
end) end)

View file

@ -1,8 +1,8 @@
local cache = require("oil.cache") local cache = require('oil.cache')
local config = require("oil.config") local config = require('oil.config')
local files = require("oil.adapters.files") local files = require('oil.adapters.files')
local fs = require("oil.fs") local fs = require('oil.fs')
local util = require("oil.util") local util = require('oil.util')
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
@ -15,7 +15,7 @@ end
---Gets the location of the home trash dir, creating it if necessary ---Gets the location of the home trash dir, creating it if necessary
---@return string ---@return string
local function get_trash_dir() local function get_trash_dir()
local trash_dir = fs.join(assert(uv.os_homedir()), ".Trash") local trash_dir = fs.join(assert(uv.os_homedir()), '.Trash')
touch_dir(trash_dir) touch_dir(trash_dir)
return trash_dir return trash_dir
end end
@ -25,7 +25,7 @@ end
M.normalize_url = function(url, callback) M.normalize_url = function(url, callback)
local scheme, path = util.parse_url(url) local scheme, path = util.parse_url(url)
assert(path) assert(path)
callback(scheme .. "/") callback(scheme .. '/')
end end
---@param url string ---@param url string
@ -34,8 +34,8 @@ end
M.get_entry_path = function(url, entry, cb) M.get_entry_path = function(url, entry, cb)
local trash_dir = get_trash_dir() local trash_dir = get_trash_dir()
local path = fs.join(trash_dir, entry.name) local path = fs.join(trash_dir, entry.name)
if entry.type == "directory" then if entry.type == 'directory' then
path = "oil://" .. path path = 'oil://' .. path
end end
cb(path) cb(path)
end end
@ -51,7 +51,7 @@ M.list = function(url, column_defs, cb)
---@diagnostic disable-next-line: param-type-mismatch, discard-returns ---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(trash_dir, function(open_err, fd) uv.fs_opendir(trash_dir, function(open_err, fd)
if open_err then if open_err then
if open_err:match("^ENOENT: no such file or directory") then if open_err:match('^ENOENT: no such file or directory') then
-- If the directory doesn't exist, treat the list as a success. We will be able to traverse -- If the directory doesn't exist, treat the list as a success. We will be able to traverse
-- and edit a not-yet-existing directory. -- and edit a not-yet-existing directory.
return cb() return cb()
@ -111,35 +111,35 @@ M.get_column = function(name)
return nil return nil
end end
M.supported_cross_adapter_actions = { files = "move" } M.supported_cross_adapter_actions = { files = 'move' }
---@param action oil.Action ---@param action oil.Action
---@return string ---@return string
M.render_action = function(action) M.render_action = function(action)
if action.type == "create" then if action.type == 'create' then
return string.format("CREATE %s", action.url) return string.format('CREATE %s', action.url)
elseif action.type == "delete" then elseif action.type == 'delete' then
return string.format(" PURGE %s", action.url) return string.format(' PURGE %s', action.url)
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))
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 src_adapter.name == "files" then if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url) local _, path = util.parse_url(action.src_url)
assert(path) assert(path)
local short_path = files.to_short_os_path(path, action.entry_type) local short_path = files.to_short_os_path(path, action.entry_type)
return string.format(" TRASH %s", short_path) return string.format(' TRASH %s', short_path)
elseif dest_adapter.name == "files" then elseif dest_adapter.name == 'files' then
local _, path = util.parse_url(action.dest_url) local _, path = util.parse_url(action.dest_url)
assert(path) assert(path)
local short_path = files.to_short_os_path(path, action.entry_type) local short_path = files.to_short_os_path(path, action.entry_type)
return string.format("RESTORE %s", short_path) return string.format('RESTORE %s', short_path)
else else
return string.format(" %s %s -> %s", action.type:upper(), action.src_url, action.dest_url) return string.format(' %s %s -> %s', action.type:upper(), action.src_url, action.dest_url)
end end
elseif action.type == "copy" then elseif action.type == 'copy' then
return string.format(" %s %s -> %s", action.type:upper(), action.src_url, action.dest_url) return string.format(' %s %s -> %s', action.type:upper(), action.src_url, action.dest_url)
else else
error("Bad action type") error('Bad action type')
end end
end end
@ -147,20 +147,20 @@ end
---@param cb fun(err: nil|string) ---@param cb fun(err: nil|string)
M.perform_action = function(action, cb) M.perform_action = function(action, cb)
local trash_dir = get_trash_dir() local trash_dir = get_trash_dir()
if action.type == "create" then if action.type == 'create' then
local _, path = util.parse_url(action.url) local _, path = util.parse_url(action.url)
assert(path) assert(path)
path = trash_dir .. path path = trash_dir .. path
if action.entry_type == "directory" then if action.entry_type == 'directory' then
uv.fs_mkdir(path, 493, function(err) uv.fs_mkdir(path, 493, function(err)
-- Ignore if the directory already exists -- Ignore if the directory already exists
if not err or err:match("^EEXIST:") then if not err or err:match('^EEXIST:') then
cb() cb()
else else
cb(err) cb(err)
end end
end) -- 0755 end) -- 0755
elseif action.entry_type == "link" and action.link then elseif action.entry_type == 'link' and action.link then
local flags = nil local flags = nil
local target = fs.posix_to_os_path(action.link) local target = fs.posix_to_os_path(action.link)
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch
@ -168,33 +168,33 @@ M.perform_action = function(action, cb)
else else
fs.touch(path, config.new_file_mode, cb) fs.touch(path, config.new_file_mode, cb)
end end
elseif action.type == "delete" then elseif action.type == 'delete' then
local _, path = util.parse_url(action.url) local _, path = util.parse_url(action.url)
assert(path) assert(path)
local fullpath = trash_dir .. path local fullpath = trash_dir .. path
fs.recursive_delete(action.entry_type, fullpath, cb) fs.recursive_delete(action.entry_type, fullpath, cb)
elseif action.type == "move" or action.type == "copy" then elseif action.type == 'move' or action.type == 'copy' 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))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
local _, src_path = util.parse_url(action.src_url) local _, src_path = util.parse_url(action.src_url)
local _, dest_path = util.parse_url(action.dest_url) local _, dest_path = util.parse_url(action.dest_url)
assert(src_path and dest_path) assert(src_path and dest_path)
if src_adapter.name == "files" then if src_adapter.name == 'files' then
dest_path = trash_dir .. dest_path dest_path = trash_dir .. dest_path
elseif dest_adapter.name == "files" then elseif dest_adapter.name == 'files' then
src_path = trash_dir .. src_path src_path = trash_dir .. src_path
else else
dest_path = trash_dir .. dest_path dest_path = trash_dir .. dest_path
src_path = trash_dir .. src_path src_path = trash_dir .. src_path
end end
if action.type == "move" then if action.type == 'move' then
fs.recursive_move(action.entry_type, src_path, dest_path, cb) fs.recursive_move(action.entry_type, src_path, dest_path, cb)
else else
fs.recursive_copy(action.entry_type, src_path, dest_path, cb) fs.recursive_copy(action.entry_type, src_path, dest_path, cb)
end end
else else
cb(string.format("Bad action type: %s", action.type)) cb(string.format('Bad action type: %s', action.type))
end end
end end
@ -212,8 +212,8 @@ M.delete_to_trash = function(path, cb)
end end
assert(src_stat) assert(src_stat)
if uv.fs_lstat(dest) then if uv.fs_lstat(dest) then
local date_str = vim.fn.strftime(" %Y-%m-%dT%H:%M:%S") local date_str = vim.fn.strftime(' %Y-%m-%dT%H:%M:%S')
local name_pieces = vim.split(basename, ".", { plain = true }) local name_pieces = vim.split(basename, '.', { plain = true })
if #name_pieces > 1 then if #name_pieces > 1 then
table.insert(name_pieces, #name_pieces - 1, date_str) table.insert(name_pieces, #name_pieces - 1, date_str)
basename = table.concat(name_pieces) basename = table.concat(name_pieces)

View file

@ -1,11 +1,11 @@
local util = require("oil.util") local util = require('oil.util')
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
local cache = require("oil.cache") local cache = require('oil.cache')
local config = require("oil.config") local config = require('oil.config')
local constants = require("oil.constants") local constants = require('oil.constants')
local files = require("oil.adapters.files") local files = require('oil.adapters.files')
local fs = require("oil.fs") local fs = require('oil.fs')
local powershell_trash = require("oil.adapters.trash.windows.powershell-trash") local powershell_trash = require('oil.adapters.trash.windows.powershell-trash')
local FIELD_META = constants.FIELD_META local FIELD_META = constants.FIELD_META
local FIELD_TYPE = constants.FIELD_TYPE local FIELD_TYPE = constants.FIELD_TYPE
@ -15,22 +15,22 @@ local M = {}
---@return string ---@return string
local function get_trash_dir() local function get_trash_dir()
local cwd = assert(vim.fn.getcwd()) local cwd = assert(vim.fn.getcwd())
local trash_dir = cwd:sub(1, 3) .. "$Recycle.Bin" local trash_dir = cwd:sub(1, 3) .. '$Recycle.Bin'
if vim.fn.isdirectory(trash_dir) == 1 then if vim.fn.isdirectory(trash_dir) == 1 then
return trash_dir return trash_dir
end end
trash_dir = "C:\\$Recycle.Bin" trash_dir = 'C:\\$Recycle.Bin'
if vim.fn.isdirectory(trash_dir) == 1 then if vim.fn.isdirectory(trash_dir) == 1 then
return trash_dir return trash_dir
end end
error("No trash found") error('No trash found')
end end
---@param path string ---@param path string
---@return string ---@return string
local win_addslash = function(path) local win_addslash = function(path)
if not vim.endswith(path, "\\") then if not vim.endswith(path, '\\') then
return path .. "\\" return path .. '\\'
else else
return path return path
end end
@ -61,7 +61,7 @@ M.list = function(url, column_defs, cb)
local raw_displayed_entries = vim.tbl_filter( local raw_displayed_entries = vim.tbl_filter(
---@param entry {IsFolder: boolean, DeletionDate: integer, Name: string, Path: string, OriginalPath: string} ---@param entry {IsFolder: boolean, DeletionDate: integer, Name: string, Path: string, OriginalPath: string}
function(entry) function(entry)
local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ":h"))) local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ':h')))
local is_in_path = path == parent local is_in_path = path == parent
local is_subpath = fs.is_subpath(path, parent) local is_subpath = fs.is_subpath(path, parent)
return is_in_path or is_subpath or show_all_files return is_in_path or is_subpath or show_all_files
@ -72,27 +72,27 @@ M.list = function(url, column_defs, cb)
---@param entry {IsFolder: boolean, DeletionDate: integer, Name: string, Path: string, OriginalPath: string} ---@param entry {IsFolder: boolean, DeletionDate: integer, Name: string, Path: string, OriginalPath: string}
---@return {[1]:nil, [2]:string, [3]:string, [4]:{stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}} ---@return {[1]:nil, [2]:string, [3]:string, [4]:{stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}}
function(entry) function(entry)
local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ":h"))) local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ':h')))
--- @type oil.InternalEntry --- @type oil.InternalEntry
local cache_entry local cache_entry
if path == parent or show_all_files then if path == parent or show_all_files then
local deleted_file_tail = assert(vim.fn.fnamemodify(entry.Path, ":t")) local deleted_file_tail = assert(vim.fn.fnamemodify(entry.Path, ':t'))
local deleted_file_head = assert(vim.fn.fnamemodify(entry.Path, ":h")) local deleted_file_head = assert(vim.fn.fnamemodify(entry.Path, ':h'))
local info_file_head = deleted_file_head local info_file_head = deleted_file_head
--- @type string? --- @type string?
local info_file local info_file
cache_entry = cache_entry =
cache.create_entry(url, deleted_file_tail, entry.IsFolder and "directory" or "file") cache.create_entry(url, deleted_file_tail, entry.IsFolder and 'directory' or 'file')
-- info_file on windows has the following format: $I<6 char hash>.<extension> -- info_file on windows has the following format: $I<6 char hash>.<extension>
-- the hash is the same for the deleted file and the info file -- the hash is the same for the deleted file and the info file
-- so, we take the hash (and extension) from the deleted file -- so, we take the hash (and extension) from the deleted file
-- --
-- see https://superuser.com/questions/368890/how-does-the-recycle-bin-in-windows-work/1736690#1736690 -- see https://superuser.com/questions/368890/how-does-the-recycle-bin-in-windows-work/1736690#1736690
local info_file_tail = deleted_file_tail:match("^%$R(.*)$") --[[@as string?]] local info_file_tail = deleted_file_tail:match('^%$R(.*)$') --[[@as string?]]
if info_file_tail then if info_file_tail then
info_file_tail = "$I" .. info_file_tail info_file_tail = '$I' .. info_file_tail
info_file = info_file_head .. "\\" .. info_file_tail info_file = info_file_head .. '\\' .. info_file_tail
end end
cache_entry[FIELD_META] = { cache_entry[FIELD_META] = {
stat = nil, stat = nil,
@ -109,10 +109,10 @@ M.list = function(url, column_defs, cb)
if path ~= parent and (show_all_files or fs.is_subpath(path, parent)) then if path ~= parent and (show_all_files or fs.is_subpath(path, parent)) then
local name = parent:sub(path:len() + 1) local name = parent:sub(path:len() + 1)
local next_par = vim.fs.dirname(name) local next_par = vim.fs.dirname(name)
while next_par ~= "." do while next_par ~= '.' do
name = next_par name = next_par
next_par = vim.fs.dirname(name) next_par = vim.fs.dirname(name)
cache_entry = cache.create_entry(url, name, "directory") cache_entry = cache.create_entry(url, name, 'directory')
cache_entry[FIELD_META] = {} cache_entry[FIELD_META] = {}
end end
@ -132,7 +132,7 @@ end
local current_year local current_year
-- Make sure we run this import-time effect in the main loop (mostly for tests) -- Make sure we run this import-time effect in the main loop (mostly for tests)
vim.schedule(function() vim.schedule(function()
current_year = vim.fn.strftime("%Y") current_year = vim.fn.strftime('%Y')
end) end)
local file_columns = {} local file_columns = {}
@ -153,11 +153,11 @@ file_columns.mtime = {
if fmt then if fmt then
ret = vim.fn.strftime(fmt, time) ret = vim.fn.strftime(fmt, time)
else else
local year = vim.fn.strftime("%Y", time) local year = vim.fn.strftime('%Y', time)
if year ~= current_year then if year ~= current_year then
ret = vim.fn.strftime("%b %d %Y", time) ret = vim.fn.strftime('%b %d %Y', time)
else else
ret = vim.fn.strftime("%b %d %H:%M", time) ret = vim.fn.strftime('%b %d %H:%M', time)
end end
end end
return ret return ret
@ -178,11 +178,11 @@ file_columns.mtime = {
local fmt = conf and conf.format local fmt = conf and conf.format
local pattern local pattern
if fmt then if fmt then
pattern = fmt:gsub("%%.", "%%S+") pattern = fmt:gsub('%%.', '%%S+')
else else
pattern = "%S+%s+%d+%s+%d%d:?%d%d" pattern = '%S+%s+%d+%s+%d%d:?%d%d'
end end
return line:match("^(" .. pattern .. ")%s+(.+)$") return line:match('^(' .. pattern .. ')%s+(.+)$')
end, end,
} }
@ -195,20 +195,21 @@ end
---@param action oil.Action ---@param action oil.Action
---@return boolean ---@return boolean
M.filter_action = function(action) M.filter_action = function(action)
if action.type == "create" then if action.type == 'create' then
return false return false
elseif action.type == "delete" then elseif action.type == 'delete' then
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]
return meta ~= nil and meta.trash_info ~= nil return meta ~= nil and meta.trash_info ~= nil
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))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
return src_adapter.name == "files" or dest_adapter.name == "files" return src_adapter.name == 'files' or dest_adapter.name == 'files'
elseif action.type == "copy" then -- selene: allow(if_same_then_else)
elseif action.type == 'copy' 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))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
return src_adapter.name == "files" or dest_adapter.name == "files" return src_adapter.name == 'files' or dest_adapter.name == 'files'
else else
error(string.format("Bad action type '%s'", action.type)) error(string.format("Bad action type '%s'", action.type))
end end
@ -219,7 +220,7 @@ end
M.normalize_url = function(url, callback) M.normalize_url = function(url, callback)
local scheme, path = util.parse_url(url) local scheme, path = util.parse_url(url)
assert(path) assert(path)
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p") local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ':p')
assert(os_path) assert(os_path)
uv.fs_realpath( uv.fs_realpath(
os_path, os_path,
@ -244,16 +245,16 @@ M.get_entry_path = function(url, entry, cb)
end end
local path = fs.os_to_posix_path(trash_info.trash_file) local path = fs.os_to_posix_path(trash_info.trash_file)
if entry.type == "directory" then if entry.type == 'directory' then
path = win_addslash(path) path = win_addslash(path)
end end
cb("oil://" .. path) cb('oil://' .. path)
end end
---@param err oil.ParseError ---@param err oil.ParseError
---@return boolean ---@return boolean
M.filter_error = function(err) M.filter_error = function(err)
if err.message == "Duplicate filename" then if err.message == 'Duplicate filename' then
return false return false
end end
return true return true
@ -262,44 +263,44 @@ end
---@param action oil.Action ---@param action oil.Action
---@return string ---@return string
M.render_action = function(action) M.render_action = function(action)
if action.type == "delete" then if action.type == 'delete' then
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 = assert(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
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter.name == "files" then if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url) local _, path = util.parse_url(action.src_url)
assert(path) assert(path)
local short_path = files.to_short_os_path(path, action.entry_type) local short_path = files.to_short_os_path(path, action.entry_type)
return string.format(" TRASH %s", short_path) return string.format(' TRASH %s', short_path)
elseif dest_adapter.name == "files" then elseif dest_adapter.name == 'files' then
local _, path = util.parse_url(action.dest_url) local _, path = util.parse_url(action.dest_url)
assert(path) assert(path)
local short_path = files.to_short_os_path(path, action.entry_type) local short_path = files.to_short_os_path(path, action.entry_type)
return string.format("RESTORE %s", short_path) return string.format('RESTORE %s', short_path)
else else
error("Must be moving files into or out of trash") error('Must be moving files into or out of trash')
end end
elseif action.type == "copy" then elseif action.type == 'copy' 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))
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 src_adapter.name == "files" then if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url) local _, path = util.parse_url(action.src_url)
assert(path) assert(path)
local short_path = files.to_short_os_path(path, action.entry_type) local short_path = files.to_short_os_path(path, action.entry_type)
return string.format(" COPY %s -> TRASH", short_path) return string.format(' COPY %s -> TRASH', short_path)
elseif dest_adapter.name == "files" then elseif dest_adapter.name == 'files' then
local _, path = util.parse_url(action.dest_url) local _, path = util.parse_url(action.dest_url)
assert(path) assert(path)
local short_path = files.to_short_os_path(path, action.entry_type) local short_path = files.to_short_os_path(path, action.entry_type)
return string.format("RESTORE %s", short_path) return string.format('RESTORE %s', short_path)
else else
error("Must be copying files into or out of trash") error('Must be copying files into or out of trash')
end end
else else
error(string.format("Bad action type '%s'", action.type)) error(string.format("Bad action type '%s'", action.type))
@ -309,11 +310,11 @@ end
---@param trash_info oil.WindowsTrashInfo ---@param trash_info oil.WindowsTrashInfo
---@param cb fun(err?: string, raw_entries: oil.WindowsRawEntry[]?) ---@param cb fun(err?: string, raw_entries: oil.WindowsRawEntry[]?)
local purge = function(trash_info, cb) local purge = function(trash_info, cb)
fs.recursive_delete("file", trash_info.info_file, function(err) fs.recursive_delete('file', trash_info.info_file, function(err)
if err then if err then
return cb(err) return cb(err)
end end
fs.recursive_delete("file", trash_info.trash_file, cb) fs.recursive_delete('file', trash_info.trash_file, cb)
end) end)
end end
@ -321,7 +322,7 @@ end
---@param type string ---@param type string
---@param cb fun(err?: string, trash_info?: oil.TrashInfo) ---@param cb fun(err?: string, trash_info?: oil.TrashInfo)
local function create_trash_info_and_copy(path, type, cb) local function create_trash_info_and_copy(path, type, cb)
local temp_path = path .. "temp" local temp_path = path .. 'temp'
-- create a temporary copy on the same location -- create a temporary copy on the same location
fs.recursive_copy( fs.recursive_copy(
type, type,
@ -346,19 +347,19 @@ end
---@param action oil.Action ---@param action oil.Action
---@param cb fun(err: nil|string) ---@param cb fun(err: nil|string)
M.perform_action = function(action, cb) M.perform_action = function(action, cb)
if action.type == "delete" then if action.type == 'delete' then
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] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]] local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
local trash_info = meta and meta.trash_info local trash_info = meta and 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))
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 src_adapter.name == "files" then if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url) local _, path = util.parse_url(action.src_url)
M.delete_to_trash(assert(path), cb) M.delete_to_trash(assert(path), cb)
elseif dest_adapter.name == "files" then elseif dest_adapter.name == 'files' then
-- Restore -- Restore
local _, dest_path = util.parse_url(action.dest_url) local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path) assert(dest_path)
@ -373,16 +374,16 @@ M.perform_action = function(action, cb)
uv.fs_unlink(trash_info.info_file, cb) uv.fs_unlink(trash_info.info_file, cb)
end) end)
end end
elseif action.type == "copy" then elseif action.type == 'copy' 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))
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 src_adapter.name == "files" then if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url) local _, path = util.parse_url(action.src_url)
assert(path) assert(path)
path = fs.posix_to_os_path(path) path = fs.posix_to_os_path(path)
local entry = assert(cache.get_entry_by_url(action.src_url)) local entry = assert(cache.get_entry_by_url(action.src_url))
create_trash_info_and_copy(path, entry[FIELD_TYPE], cb) create_trash_info_and_copy(path, entry[FIELD_TYPE], cb)
elseif dest_adapter.name == "files" then elseif dest_adapter.name == 'files' then
-- Restore -- Restore
local _, dest_path = util.parse_url(action.dest_url) local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path) assert(dest_path)
@ -392,14 +393,14 @@ M.perform_action = function(action, cb)
local trash_info = meta and meta.trash_info local trash_info = meta and 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')
end end
else else
cb(string.format("Bad action type: %s", action.type)) cb(string.format('Bad action type: %s', action.type))
end end
end end
M.supported_cross_adapter_actions = { files = "move" } M.supported_cross_adapter_actions = { files = 'move' }
---@param path string ---@param path string
---@param cb fun(err?: string) ---@param cb fun(err?: string)

View file

@ -41,23 +41,23 @@ function PowershellConnection:_init(init_command)
-- 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({
"cmd", 'cmd',
"/c", '/c',
'"chcp 65001 && powershell -NoProfile -NoLogo -ExecutionPolicy Bypass -NoExit -Command -"', '"chcp 65001 && powershell -NoProfile -NoLogo -ExecutionPolicy Bypass -NoExit -Command -"',
}, { }, {
---@param data string[] ---@param data string[]
on_stdout = function(_, data) on_stdout = function(_, data)
for _, fragment in ipairs(data) do for _, fragment in ipairs(data) do
if fragment:find("===DONE%((%a+)%)===") then if fragment:find('===DONE%((%a+)%)===') then
self.is_reading_data = false self.is_reading_data = false
local output = table.concat(self.stdout, "") local output = table.concat(self.stdout, '')
local cb = self.commands[1].cb local cb = self.commands[1].cb
table.remove(self.commands, 1) table.remove(self.commands, 1)
local success = fragment:match("===DONE%((%a+)%)===") local success = fragment:match('===DONE%((%a+)%)===')
if success == "True" then if success == 'True' then
cb(nil, output) cb(nil, output)
elseif success == "False" then elseif success == 'False' then
cb(success .. ": " .. output, output) cb(success .. ': ' .. output, output)
end end
self.stdout = {} self.stdout = {}
self:_consume() self:_consume()

View file

@ -1,5 +1,5 @@
-- A wrapper around trash operations using windows powershell -- A wrapper around trash operations using windows powershell
local Powershell = require("oil.adapters.trash.windows.powershell-connection") local Powershell = require('oil.adapters.trash.windows.powershell-connection')
---@class oil.WindowsRawEntry ---@class oil.WindowsRawEntry
---@field IsFolder boolean ---@field IsFolder boolean

View file

@ -1,5 +1,5 @@
local constants = require("oil.constants") local constants = require('oil.constants')
local util = require("oil.util") local util = require('oil.util')
local M = {} local M = {}
local FIELD_ID = constants.FIELD_ID local FIELD_ID = constants.FIELD_ID
@ -28,7 +28,7 @@ local _cached_id_fmt
M.format_id = function(id) M.format_id = function(id)
if not _cached_id_fmt then if not _cached_id_fmt then
local id_str_length = math.max(3, 1 + math.floor(math.log10(next_id))) local id_str_length = math.max(3, 1 + math.floor(math.log10(next_id)))
_cached_id_fmt = "/%0" .. string.format("%d", id_str_length) .. "d" _cached_id_fmt = '/%0' .. string.format('%d', id_str_length) .. 'd'
end end
return _cached_id_fmt:format(id) return _cached_id_fmt:format(id)
end end
@ -125,8 +125,8 @@ end
M.get_entry_by_url = function(url) M.get_entry_by_url = function(url)
local scheme, path = util.parse_url(url) local scheme, path = util.parse_url(url)
assert(path) assert(path)
local parent_url = scheme .. vim.fn.fnamemodify(path, ":h") local parent_url = scheme .. vim.fn.fnamemodify(path, ':h')
local basename = vim.fn.fnamemodify(path, ":t") local basename = vim.fn.fnamemodify(path, ':t')
return M.list_url(parent_url)[basename] return M.list_url(parent_url)[basename]
end end
@ -135,7 +135,7 @@ end
M.get_parent_url = function(id) M.get_parent_url = function(id)
local url = parent_url_by_id[id] local url = parent_url_by_id[id]
if not url then if not url then
error(string.format("Entry %d missing parent url", id)) error(string.format('Entry %d missing parent url', id))
end end
return url return url
end end
@ -149,32 +149,32 @@ end
---@param action oil.Action ---@param action oil.Action
M.perform_action = function(action) M.perform_action = function(action)
if action.type == "create" then if action.type == 'create' then
local scheme, path = util.parse_url(action.url) local scheme, path = util.parse_url(action.url)
assert(path) assert(path)
local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h")) local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ':h'))
local name = vim.fn.fnamemodify(path, ":t") local name = vim.fn.fnamemodify(path, ':t')
M.create_and_store_entry(parent_url, name, action.entry_type) M.create_and_store_entry(parent_url, name, action.entry_type)
elseif action.type == "delete" then elseif action.type == 'delete' then
local scheme, path = util.parse_url(action.url) local scheme, path = util.parse_url(action.url)
assert(path) assert(path)
local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h")) local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ':h'))
local name = vim.fn.fnamemodify(path, ":t") local name = vim.fn.fnamemodify(path, ':t')
local entry = url_directory[parent_url][name] local entry = url_directory[parent_url][name]
url_directory[parent_url][name] = nil url_directory[parent_url][name] = nil
entries_by_id[entry[FIELD_ID]] = nil entries_by_id[entry[FIELD_ID]] = nil
parent_url_by_id[entry[FIELD_ID]] = nil parent_url_by_id[entry[FIELD_ID]] = nil
elseif action.type == "move" then elseif action.type == 'move' then
local src_scheme, src_path = util.parse_url(action.src_url) local src_scheme, src_path = util.parse_url(action.src_url)
assert(src_path) assert(src_path)
local src_parent_url = util.addslash(src_scheme .. vim.fn.fnamemodify(src_path, ":h")) local src_parent_url = util.addslash(src_scheme .. vim.fn.fnamemodify(src_path, ':h'))
local src_name = vim.fn.fnamemodify(src_path, ":t") local src_name = vim.fn.fnamemodify(src_path, ':t')
local entry = url_directory[src_parent_url][src_name] local entry = url_directory[src_parent_url][src_name]
local dest_scheme, dest_path = util.parse_url(action.dest_url) local dest_scheme, dest_path = util.parse_url(action.dest_url)
assert(dest_path) assert(dest_path)
local dest_parent_url = util.addslash(dest_scheme .. vim.fn.fnamemodify(dest_path, ":h")) local dest_parent_url = util.addslash(dest_scheme .. vim.fn.fnamemodify(dest_path, ':h'))
local dest_name = vim.fn.fnamemodify(dest_path, ":t") local dest_name = vim.fn.fnamemodify(dest_path, ':t')
url_directory[src_parent_url][src_name] = nil url_directory[src_parent_url][src_name] = nil
local dest_parent = url_directory[dest_parent_url] local dest_parent = url_directory[dest_parent_url]
@ -188,13 +188,14 @@ M.perform_action = function(action)
parent_url_by_id[entry[FIELD_ID]] = dest_parent_url parent_url_by_id[entry[FIELD_ID]] = dest_parent_url
entry[FIELD_NAME] = dest_name entry[FIELD_NAME] = dest_name
util.update_moved_buffers(action.entry_type, action.src_url, action.dest_url) util.update_moved_buffers(action.entry_type, action.src_url, action.dest_url)
elseif action.type == "copy" then elseif action.type == 'copy' then
local scheme, path = util.parse_url(action.dest_url) local scheme, path = util.parse_url(action.dest_url)
assert(path) assert(path)
local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h")) local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ':h'))
local name = vim.fn.fnamemodify(path, ":t") local name = vim.fn.fnamemodify(path, ':t')
M.create_and_store_entry(parent_url, name, action.entry_type) M.create_and_store_entry(parent_url, name, action.entry_type)
elseif action.type == "change" then -- selene: allow(empty_if)
elseif action.type == 'change' then
-- Cache doesn't need to update -- Cache doesn't need to update
else else
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field

View file

@ -1,11 +1,11 @@
local cache = require("oil.cache") local cache = require('oil.cache')
local columns = require("oil.columns") local columns = require('oil.columns')
local config = require("oil.config") local config = require('oil.config')
local fs = require("oil.fs") local fs = require('oil.fs')
local oil = require("oil") local oil = require('oil')
local parser = require("oil.mutator.parser") local parser = require('oil.mutator.parser')
local util = require("oil.util") local util = require('oil.util')
local view = require("oil.view") local view = require('oil.view')
local M = {} local M = {}
@ -16,10 +16,10 @@ local function get_linux_session_type()
return return
end end
xdg_session_type = xdg_session_type:lower() xdg_session_type = xdg_session_type:lower()
if xdg_session_type:find("x11") then if xdg_session_type:find('x11') then
return "x11" return 'x11'
elseif xdg_session_type:find("wayland") then elseif xdg_session_type:find('wayland') then
return "wayland" return 'wayland'
else else
return nil return nil
end end
@ -29,9 +29,9 @@ end
local function is_linux_desktop_gnome() local function is_linux_desktop_gnome()
local cur_desktop = vim.env.XDG_CURRENT_DESKTOP local cur_desktop = vim.env.XDG_CURRENT_DESKTOP
local session_desktop = vim.env.XDG_SESSION_DESKTOP local session_desktop = vim.env.XDG_SESSION_DESKTOP
local idx = session_desktop and session_desktop:lower():find("gnome") local idx = session_desktop and session_desktop:lower():find('gnome')
or cur_desktop and cur_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" return idx ~= nil or cur_desktop == 'X-Cinnamon' or cur_desktop == 'XFCE'
end end
---@param winid integer ---@param winid integer
@ -55,7 +55,7 @@ end
---@param entry oil.InternalEntry ---@param entry oil.InternalEntry
local function remove_entry_from_parent_buffer(parent_url, entry) local function remove_entry_from_parent_buffer(parent_url, entry)
local bufnr = vim.fn.bufadd(parent_url) local bufnr = vim.fn.bufadd(parent_url)
assert(vim.api.nvim_buf_is_loaded(bufnr), "Expected parent buffer to be loaded during paste") assert(vim.api.nvim_buf_is_loaded(bufnr), 'Expected parent buffer to be loaded during paste')
local adapter = assert(util.get_adapter(bufnr)) local adapter = assert(util.get_adapter(bufnr))
local column_defs = columns.get_supported_columns(adapter) local column_defs = columns.get_supported_columns(adapter)
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
@ -77,7 +77,7 @@ end
---@param delete_original? boolean ---@param delete_original? boolean
local function paste_paths(paths, delete_original) local function paste_paths(paths, delete_original)
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local scheme = "oil://" local scheme = 'oil://'
local adapter = assert(config.get_adapter_by_scheme(scheme)) local adapter = assert(config.get_adapter_by_scheme(scheme))
local column_defs = columns.get_supported_columns(scheme) local column_defs = columns.get_supported_columns(scheme)
local winid = vim.api.nvim_get_current_win() local winid = vim.api.nvim_get_current_win()
@ -88,7 +88,7 @@ local function paste_paths(paths, delete_original)
-- Handle as many paths synchronously as possible -- Handle as many paths synchronously as possible
for _, path in ipairs(paths) do for _, path in ipairs(paths) do
-- Trim the trailing slash off directories -- Trim the trailing slash off directories
if vim.endswith(path, "/") then if vim.endswith(path, '/') then
path = path:sub(1, -2) path = path:sub(1, -2)
end end
@ -114,7 +114,7 @@ local function paste_paths(paths, delete_original)
local cursor = vim.api.nvim_win_get_cursor(winid) local cursor = vim.api.nvim_win_get_cursor(winid)
local complete_loading = util.cb_collect(#vim.tbl_keys(parent_urls), function(err) local complete_loading = util.cb_collect(#vim.tbl_keys(parent_urls), function(err)
if err then if err then
vim.notify(string.format("Error loading parent directory: %s", err), vim.log.levels.ERROR) vim.notify(string.format('Error loading parent directory: %s', err), vim.log.levels.ERROR)
else else
-- Something in this process moves the cursor to the top of the window, so have to restore it -- 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) vim.api.nvim_win_set_cursor(winid, cursor)
@ -149,8 +149,8 @@ end
---@return integer end ---@return integer end
local function range_from_selection() local function range_from_selection()
-- [bufnum, lnum, col, off]; both row and column 1-indexed -- [bufnum, lnum, col, off]; both row and column 1-indexed
local start = vim.fn.getpos("v") local start = vim.fn.getpos('v')
local end_ = vim.fn.getpos(".") local end_ = vim.fn.getpos('.')
local start_row = start[2] local start_row = start[2]
local end_row = end_[2] local end_row = end_[2]
@ -164,16 +164,16 @@ end
M.copy_to_system_clipboard = function() M.copy_to_system_clipboard = function()
local dir = oil.get_current_dir() local dir = oil.get_current_dir()
if not dir then if not dir then
vim.notify("System clipboard only works for local files", vim.log.levels.ERROR) vim.notify('System clipboard only works for local files', vim.log.levels.ERROR)
return return
end end
local entries = {} local entries = {}
local mode = vim.api.nvim_get_mode().mode local mode = vim.api.nvim_get_mode().mode
if mode == "v" or mode == "V" then if mode == 'v' or mode == 'V' then
if fs.is_mac then if fs.is_mac then
vim.notify( vim.notify(
"Copying multiple paths to clipboard is not supported on mac", 'Copying multiple paths to clipboard is not supported on mac',
vim.log.levels.ERROR vim.log.levels.ERROR
) )
return return
@ -184,7 +184,7 @@ M.copy_to_system_clipboard = function()
end end
-- leave visual mode -- leave visual mode
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", true, false, true), "n", true) vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<Esc>', true, false, true), 'n', true)
else else
table.insert(entries, oil.get_cursor_entry()) table.insert(entries, oil.get_cursor_entry())
end end
@ -193,7 +193,7 @@ M.copy_to_system_clipboard = function()
entries = vim.tbl_values(entries) entries = vim.tbl_values(entries)
if #entries == 0 then if #entries == 0 then
vim.notify("Could not find local file under cursor", vim.log.levels.WARN) vim.notify('Could not find local file under cursor', vim.log.levels.WARN)
return return
end end
local paths = {} local paths = {}
@ -204,38 +204,38 @@ M.copy_to_system_clipboard = function()
local stdin local stdin
if fs.is_mac then if fs.is_mac then
cmd = { cmd = {
"osascript", 'osascript',
"-e", '-e',
"on run args", 'on run args',
"-e", '-e',
"set the clipboard to POSIX file (first item of args)", 'set the clipboard to POSIX file (first item of args)',
"-e", '-e',
"end run", 'end run',
paths[1], paths[1],
} }
elseif fs.is_linux then elseif fs.is_linux then
local xdg_session_type = get_linux_session_type() local xdg_session_type = get_linux_session_type()
if xdg_session_type == "x11" then if xdg_session_type == 'x11' then
vim.list_extend(cmd, { "xclip", "-i", "-selection", "clipboard" }) vim.list_extend(cmd, { 'xclip', '-i', '-selection', 'clipboard' })
elseif xdg_session_type == "wayland" then elseif xdg_session_type == 'wayland' then
table.insert(cmd, "wl-copy") table.insert(cmd, 'wl-copy')
else else
vim.notify("System clipboard not supported, check $XDG_SESSION_TYPE", vim.log.levels.ERROR) vim.notify('System clipboard not supported, check $XDG_SESSION_TYPE', vim.log.levels.ERROR)
return return
end end
local urls = {} local urls = {}
for _, path in ipairs(paths) do for _, path in ipairs(paths) do
table.insert(urls, "file://" .. path) table.insert(urls, 'file://' .. path)
end end
if is_linux_desktop_gnome() then if is_linux_desktop_gnome() then
stdin = string.format("copy\n%s\0", table.concat(urls, "\n")) stdin = string.format('copy\n%s\0', table.concat(urls, '\n'))
vim.list_extend(cmd, { "-t", "x-special/gnome-copied-files" }) vim.list_extend(cmd, { '-t', 'x-special/gnome-copied-files' })
else else
stdin = table.concat(urls, "\n") .. "\n" stdin = table.concat(urls, '\n') .. '\n'
vim.list_extend(cmd, { "-t", "text/uri-list" }) vim.list_extend(cmd, { '-t', 'text/uri-list' })
end end
else else
vim.notify("System clipboard not supported on Windows", vim.log.levels.ERROR) vim.notify('System clipboard not supported on Windows', vim.log.levels.ERROR)
return return
end end
@ -243,11 +243,11 @@ M.copy_to_system_clipboard = function()
vim.notify(string.format("Could not find executable '%s'", cmd[1]), vim.log.levels.ERROR) vim.notify(string.format("Could not find executable '%s'", cmd[1]), vim.log.levels.ERROR)
return return
end end
local stderr = "" local stderr = ''
local jid = vim.fn.jobstart(cmd, { local jid = vim.fn.jobstart(cmd, {
stderr_buffered = true, stderr_buffered = true,
on_stderr = function(_, data) on_stderr = function(_, data)
stderr = table.concat(data, "\n") stderr = table.concat(data, '\n')
end, end,
on_exit = function(j, exit_code) on_exit = function(j, exit_code)
if exit_code ~= 0 then if exit_code ~= 0 then
@ -259,15 +259,15 @@ M.copy_to_system_clipboard = function()
if #paths == 1 then if #paths == 1 then
vim.notify(string.format("Copied '%s' to system clipboard", paths[1])) vim.notify(string.format("Copied '%s' to system clipboard", paths[1]))
else else
vim.notify(string.format("Copied %d files to system clipboard", #paths)) vim.notify(string.format('Copied %d files to system clipboard', #paths))
end end
end end
end, end,
}) })
assert(jid > 0, "Failed to start job") assert(jid > 0, 'Failed to start job')
if stdin then if stdin then
vim.api.nvim_chan_send(jid, stdin) vim.api.nvim_chan_send(jid, stdin)
vim.fn.chanclose(jid, "stdin") vim.fn.chanclose(jid, 'stdin')
end end
end end
@ -276,7 +276,7 @@ end
local function handle_paste_output_mac(lines) local function handle_paste_output_mac(lines)
local ret = {} local ret = {}
for _, line in ipairs(lines) do for _, line in ipairs(lines) do
if not line:match("^%s*$") then if not line:match('^%s*$') then
table.insert(ret, line) table.insert(ret, line)
end end
end end
@ -288,7 +288,7 @@ end
local function handle_paste_output_linux(lines) local function handle_paste_output_linux(lines)
local ret = {} local ret = {}
for _, line in ipairs(lines) do for _, line in ipairs(lines) do
local path = line:match("^file://(.+)$") local path = line:match('^file://(.+)$')
if path then if path then
table.insert(ret, util.url_unescape(path)) table.insert(ret, util.url_unescape(path))
end end
@ -306,37 +306,37 @@ M.paste_from_system_clipboard = function(delete_original)
local handle_paste_output local handle_paste_output
if fs.is_mac then if fs.is_mac then
cmd = { cmd = {
"osascript", 'osascript',
"-e", '-e',
"on run", 'on run',
"-e", '-e',
"POSIX path of (the clipboard as «class furl»)", 'POSIX path of (the clipboard as «class furl»)',
"-e", '-e',
"end run", 'end run',
} }
handle_paste_output = handle_paste_output_mac handle_paste_output = handle_paste_output_mac
elseif fs.is_linux then elseif fs.is_linux then
local xdg_session_type = get_linux_session_type() local xdg_session_type = get_linux_session_type()
if xdg_session_type == "x11" then if xdg_session_type == 'x11' then
vim.list_extend(cmd, { "xclip", "-o", "-selection", "clipboard" }) vim.list_extend(cmd, { 'xclip', '-o', '-selection', 'clipboard' })
elseif xdg_session_type == "wayland" then elseif xdg_session_type == 'wayland' then
table.insert(cmd, "wl-paste") table.insert(cmd, 'wl-paste')
else else
vim.notify("System clipboard not supported, check $XDG_SESSION_TYPE", vim.log.levels.ERROR) vim.notify('System clipboard not supported, check $XDG_SESSION_TYPE', vim.log.levels.ERROR)
return return
end end
if is_linux_desktop_gnome() then if is_linux_desktop_gnome() then
vim.list_extend(cmd, { "-t", "x-special/gnome-copied-files" }) vim.list_extend(cmd, { '-t', 'x-special/gnome-copied-files' })
else else
vim.list_extend(cmd, { "-t", "text/uri-list" }) vim.list_extend(cmd, { '-t', 'text/uri-list' })
end end
handle_paste_output = handle_paste_output_linux handle_paste_output = handle_paste_output_linux
else else
vim.notify("System clipboard not supported on Windows", vim.log.levels.ERROR) vim.notify('System clipboard not supported on Windows', vim.log.levels.ERROR)
return return
end end
local paths local paths
local stderr = "" local stderr = ''
if vim.fn.executable(cmd[1]) == 0 then if vim.fn.executable(cmd[1]) == 0 then
vim.notify(string.format("Could not find executable '%s'", cmd[1]), vim.log.levels.ERROR) vim.notify(string.format("Could not find executable '%s'", cmd[1]), vim.log.levels.ERROR)
return return
@ -345,26 +345,26 @@ M.paste_from_system_clipboard = function(delete_original)
stdout_buffered = true, stdout_buffered = true,
stderr_buffered = true, stderr_buffered = true,
on_stdout = function(j, data) on_stdout = function(j, data)
local lines = vim.split(table.concat(data, "\n"), "\r?\n") local lines = vim.split(table.concat(data, '\n'), '\r?\n')
paths = handle_paste_output(lines) paths = handle_paste_output(lines)
end, end,
on_stderr = function(_, data) on_stderr = function(_, data)
stderr = table.concat(data, "\n") stderr = table.concat(data, '\n')
end, end,
on_exit = function(j, exit_code) on_exit = function(j, exit_code)
if exit_code ~= 0 or not paths then if exit_code ~= 0 or not paths then
vim.notify( vim.notify(
string.format("Error pasting from system clipboard: %s", stderr), string.format('Error pasting from system clipboard: %s', stderr),
vim.log.levels.ERROR vim.log.levels.ERROR
) )
elseif #paths == 0 then elseif #paths == 0 then
vim.notify("No valid files found in system clipboard", vim.log.levels.WARN) vim.notify('No valid files found in system clipboard', vim.log.levels.WARN)
else else
paste_paths(paths, delete_original) paste_paths(paths, delete_original)
end end
end, end,
}) })
assert(jid > 0, "Failed to start job") assert(jid > 0, 'Failed to start job')
end end
return M return M

View file

@ -1,6 +1,6 @@
local config = require("oil.config") local config = require('oil.config')
local constants = require("oil.constants") local constants = require('oil.constants')
local util = require("oil.util") local util = require('oil.util')
local M = {} local M = {}
local FIELD_NAME = constants.FIELD_NAME local FIELD_NAME = constants.FIELD_NAME
@ -38,7 +38,7 @@ end
---@return oil.ColumnSpec[] ---@return oil.ColumnSpec[]
M.get_supported_columns = function(adapter_or_scheme) M.get_supported_columns = function(adapter_or_scheme)
local adapter local adapter
if type(adapter_or_scheme) == "string" then if type(adapter_or_scheme) == 'string' then
adapter = config.get_adapter_by_scheme(adapter_or_scheme) adapter = config.get_adapter_by_scheme(adapter_or_scheme)
else else
adapter = adapter_or_scheme adapter = adapter_or_scheme
@ -53,7 +53,7 @@ M.get_supported_columns = function(adapter_or_scheme)
return ret return ret
end end
local EMPTY = { "-", "OilEmpty" } local EMPTY = { '-', 'OilEmpty' }
M.EMPTY = EMPTY M.EMPTY = EMPTY
@ -71,17 +71,17 @@ M.render_col = function(adapter, col_def, entry, bufnr)
end end
local chunk = column.render(entry, conf, bufnr) local chunk = column.render(entry, conf, bufnr)
if type(chunk) == "table" then if type(chunk) == 'table' then
if chunk[1]:match("^%s*$") then if chunk[1]:match('^%s*$') then
return EMPTY return EMPTY
end end
else else
if not chunk or chunk:match("^%s*$") then if not chunk or chunk:match('^%s*$') then
return EMPTY return EMPTY
end end
if conf and conf.highlight then if conf and conf.highlight then
local highlight = conf.highlight local highlight = conf.highlight
if type(highlight) == "function" then if type(highlight) == 'function' then
highlight = conf.highlight(chunk) highlight = conf.highlight(chunk)
end end
return { chunk, highlight } return { chunk, highlight }
@ -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*(-%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:gsub("^%s+", ""), conf) return column.parse(line:gsub('^%s+', ''), conf)
end end
end end
@ -128,12 +128,12 @@ end
M.render_change_action = function(adapter, action) M.render_change_action = function(adapter, action)
local column = M.get_column(adapter, action.column) local column = M.get_column(adapter, action.column)
if not column then if not column then
error(string.format("Received change action for nonexistant column %s", action.column)) error(string.format('Received change action for nonexistant column %s', action.column))
end end
if column.render_action then if column.render_action then
return column.render_action(action) return column.render_action(action)
else else
return string.format("CHANGE %s %s = %s", action.url, action.column, action.value) return string.format('CHANGE %s %s = %s', action.url, action.column, action.value)
end end
end end
@ -144,7 +144,7 @@ M.perform_change_action = function(adapter, action, callback)
local column = M.get_column(adapter, action.column) local column = M.get_column(adapter, action.column)
if not column then if not column then
return callback( return callback(
string.format("Received change action for nonexistant column %s", action.column) string.format('Received change action for nonexistant column %s', action.column)
) )
end end
column.perform_action(action, callback) column.perform_action(action, callback)
@ -152,12 +152,12 @@ end
local icon_provider = util.get_icon_provider() local icon_provider = util.get_icon_provider()
if icon_provider then if icon_provider then
M.register("icon", { M.register('icon', {
render = function(entry, conf, bufnr) render = function(entry, conf, bufnr)
local field_type = entry[FIELD_TYPE] local field_type = entry[FIELD_TYPE]
local name = entry[FIELD_NAME] local name = entry[FIELD_NAME]
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
if field_type == "link" and meta then if field_type == 'link' and meta then
if meta.link then if meta.link then
name = meta.link name = meta.link
end end
@ -170,11 +170,11 @@ if icon_provider then
end end
local ft = nil local ft = nil
if conf and conf.use_slow_filetype_detection and field_type == "file" then if conf and conf.use_slow_filetype_detection and field_type == 'file' then
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local _, path = util.parse_url(bufname) local _, path = util.parse_url(bufname)
if path then if path then
local lines = vim.fn.readfile(path .. name, "", 16) local lines = vim.fn.readfile(path .. name, '', 16)
if lines and #lines > 0 then if lines and #lines > 0 then
ft = vim.filetype.match({ filename = name, contents = lines }) ft = vim.filetype.match({ filename = name, contents = lines })
end end
@ -183,10 +183,10 @@ if icon_provider then
local icon, hl = icon_provider(field_type, name, conf, ft) local icon, hl = icon_provider(field_type, name, conf, ft)
if not conf or conf.add_padding ~= false then if not conf or conf.add_padding ~= false then
icon = icon .. " " icon = icon .. ' '
end end
if conf and conf.highlight then if conf and conf.highlight then
if type(conf.highlight) == "function" then if type(conf.highlight) == 'function' then
hl = conf.highlight(icon) hl = conf.highlight(icon)
else else
hl = conf.highlight hl = conf.highlight
@ -196,29 +196,29 @@ if icon_provider then
end, end,
parse = function(line, conf) parse = function(line, conf)
return line:match("^(%S+)%s+(.*)$") return line:match('^(%S+)%s+(.*)$')
end, end,
}) })
end end
local default_type_icons = { local default_type_icons = {
directory = "dir", directory = 'dir',
socket = "sock", socket = 'sock',
} }
---@param entry oil.InternalEntry ---@param entry oil.InternalEntry
---@return boolean ---@return boolean
local function is_entry_directory(entry) local function is_entry_directory(entry)
local type = entry[FIELD_TYPE] local type = entry[FIELD_TYPE]
if type == "directory" then if type == 'directory' then
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") == true return (meta and meta.link_stat and meta.link_stat.type == 'directory') == true
else else
return false return false
end end
end end
M.register("type", { M.register('type', {
render = function(entry, conf) render = function(entry, conf)
local entry_type = entry[FIELD_TYPE] local entry_type = entry[FIELD_TYPE]
if conf and conf.icons then if conf and conf.icons then
@ -229,7 +229,7 @@ M.register("type", {
end, end,
parse = function(line, conf) parse = function(line, conf)
return line:match("^(%S+)%s+(.*)$") return line:match('^(%S+)%s+(.*)$')
end, end,
get_sort_value = function(entry) get_sort_value = function(entry)
@ -242,22 +242,22 @@ M.register("type", {
}) })
local function adjust_number(int) local function adjust_number(int)
return string.format("%03d%s", #int, int) return string.format('%03d%s', #int, int)
end end
M.register("name", { M.register('name', {
render = function(entry, conf) render = function(entry, conf)
error("Do not use the name column. It is for sorting only") error('Do not use the name column. It is for sorting only')
end, end,
parse = function(line, conf) parse = function(line, conf)
error("Do not use the name column. It is for sorting only") error('Do not use the name column. It is for sorting only')
end, end,
create_sort_value_factory = function(num_entries) create_sort_value_factory = function(num_entries)
if if
config.view_options.natural_order == false config.view_options.natural_order == false
or (config.view_options.natural_order == "fast" and num_entries > 5000) or (config.view_options.natural_order == 'fast' and num_entries > 5000)
then then
if config.view_options.case_insensitive then if config.view_options.case_insensitive then
return function(entry) return function(entry)
@ -272,7 +272,7 @@ M.register("name", {
local memo = {} local memo = {}
return function(entry) return function(entry)
if memo[entry] == nil then if memo[entry] == nil then
local name = entry[FIELD_NAME]:gsub("0*(%d+)", adjust_number) local name = entry[FIELD_NAME]:gsub('0*(%d+)', adjust_number)
if config.view_options.case_insensitive then if config.view_options.case_insensitive then
name = name:lower() name = name:lower()
end end

View file

@ -5,7 +5,7 @@ local default_config = {
-- Id is automatically added at the beginning, and name at the end -- Id is automatically added at the beginning, and name at the end
-- See :help oil-columns -- See :help oil-columns
columns = { columns = {
"icon", 'icon',
-- "permissions", -- "permissions",
-- "size", -- "size",
-- "mtime", -- "mtime",
@ -13,18 +13,18 @@ local default_config = {
-- Buffer-local options to use for oil buffers -- Buffer-local options to use for oil buffers
buf_options = { buf_options = {
buflisted = false, buflisted = false,
bufhidden = "hide", bufhidden = 'hide',
}, },
-- Window-local options to use for oil buffers -- Window-local options to use for oil buffers
win_options = { win_options = {
wrap = false, wrap = false,
signcolumn = "no", signcolumn = 'no',
cursorcolumn = false, cursorcolumn = false,
foldcolumn = "0", foldcolumn = '0',
spell = false, spell = false,
list = false, list = false,
conceallevel = 3, conceallevel = 3,
concealcursor = "nvic", concealcursor = 'nvic',
}, },
-- Send deleted files to the trash instead of permanently deleting them (:help oil-trash) -- Send deleted files to the trash instead of permanently deleting them (:help oil-trash)
delete_to_trash = false, delete_to_trash = false,
@ -48,7 +48,7 @@ local default_config = {
}, },
-- Constrain the cursor to the editable parts of the oil buffer -- Constrain the cursor to the editable parts of the oil buffer
-- Set to `false` to disable, or "name" to keep it on the file names -- Set to `false` to disable, or "name" to keep it on the file names
constrain_cursor = "editable", constrain_cursor = 'editable',
-- Set to true to watch the filesystem for changes and reload oil -- Set to true to watch the filesystem for changes and reload oil
watch_for_changes = false, watch_for_changes = false,
-- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap -- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap
@ -58,22 +58,22 @@ local default_config = {
-- Set to `false` to remove a keymap -- Set to `false` to remove a keymap
-- See :help oil-actions for a list of all available actions -- See :help oil-actions for a list of all available actions
keymaps = { keymaps = {
["g?"] = { "actions.show_help", mode = "n" }, ['g?'] = { 'actions.show_help', mode = 'n' },
["<CR>"] = "actions.select", ['<CR>'] = 'actions.select',
["<C-s>"] = { "actions.select", opts = { vertical = true } }, ['<C-s>'] = { 'actions.select', opts = { vertical = true } },
["<C-h>"] = { "actions.select", opts = { horizontal = true } }, ['<C-h>'] = { 'actions.select', opts = { horizontal = true } },
["<C-t>"] = { "actions.select", opts = { tab = true } }, ['<C-t>'] = { 'actions.select', opts = { tab = true } },
["<C-p>"] = "actions.preview", ['<C-p>'] = 'actions.preview',
["<C-c>"] = { "actions.close", mode = "n" }, ['<C-c>'] = { 'actions.close', mode = 'n' },
["<C-l>"] = "actions.refresh", ['<C-l>'] = 'actions.refresh',
["-"] = { "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' },
["g~"] = { "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' },
["g\\"] = { "actions.toggle_trash", mode = "n" }, ['g\\'] = { 'actions.toggle_trash', mode = 'n' },
}, },
-- Set to false to disable all of the above keymaps -- Set to false to disable all of the above keymaps
use_default_keymaps = true, use_default_keymaps = true,
@ -82,7 +82,7 @@ local default_config = {
show_hidden = false, show_hidden = false,
-- This function defines what is considered a "hidden" file -- This function defines what is considered a "hidden" file
is_hidden_file = function(name, bufnr) is_hidden_file = function(name, bufnr)
local m = name:match("^%.") local m = name:match('^%.')
return m ~= nil return m ~= nil
end, end,
-- This function defines what will never be shown, even when `show_hidden` is set -- This function defines what will never be shown, even when `show_hidden` is set
@ -91,14 +91,14 @@ local default_config = {
end, end,
-- Sort file names with numbers in a more intuitive order for humans. -- Sort file names with numbers in a more intuitive order for humans.
-- Can be "fast", true, or false. "fast" will turn it off for large directories. -- Can be "fast", true, or false. "fast" will turn it off for large directories.
natural_order = "fast", natural_order = 'fast',
-- Sort file and directory names case insensitive -- Sort file and directory names case insensitive
case_insensitive = false, case_insensitive = false,
sort = { sort = {
-- sort order can be "asc" or "desc" -- sort order can be "asc" or "desc"
-- see :help oil-columns to see which columns are sortable -- see :help oil-columns to see which columns are sortable
{ "type", "asc" }, { 'type', 'asc' },
{ "name", "asc" }, { 'name', 'asc' },
}, },
-- Customize the highlight group for the file name -- Customize the highlight group for the file name
highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan) highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan)
@ -138,7 +138,7 @@ local default_config = {
-- optionally override the oil buffers window title with custom function: fun(winid: integer): string -- optionally override the oil buffers window title with custom function: fun(winid: integer): string
get_win_title = nil, get_win_title = nil,
-- preview_split: Split direction: "auto", "left", "right", "above", "below". -- preview_split: Split direction: "auto", "left", "right", "above", "below".
preview_split = "auto", preview_split = 'auto',
-- This is the config that will be passed to nvim_open_win. -- This is the config that will be passed to nvim_open_win.
-- Change values here to customize the layout -- Change values here to customize the layout
override = function(conf) override = function(conf)
@ -150,7 +150,7 @@ local default_config = {
-- Whether the preview window is automatically updated when the cursor is moved -- Whether the preview window is automatically updated when the cursor is moved
update_on_cursor_moved = true, update_on_cursor_moved = true,
-- How to open the preview window "load"|"scratch"|"fast_scratch" -- How to open the preview window "load"|"scratch"|"fast_scratch"
preview_method = "fast_scratch", preview_method = 'fast_scratch',
-- A function that returns true to disable preview on a file e.g. to avoid lag -- A function that returns true to disable preview on a file e.g. to avoid lag
disable_preview = function(filename) disable_preview = function(filename)
return false return false
@ -190,7 +190,7 @@ local default_config = {
min_height = { 5, 0.1 }, min_height = { 5, 0.1 },
height = nil, height = nil,
border = nil, border = nil,
minimized_border = "none", minimized_border = 'none',
win_options = { win_options = {
winblend = 0, winblend = 0,
}, },
@ -211,12 +211,12 @@ local default_config = {
-- not "oil-s3://" on older neovim versions, since it doesn't open buffers correctly with a number -- not "oil-s3://" on older neovim versions, since it doesn't open buffers correctly with a number
-- in the name -- in the name
local oil_s3_string = vim.fn.has("nvim-0.12") == 1 and "oil-s3://" or "oil-sss://" 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_s3_string] = 's3',
["oil-trash://"] = "trash", ['oil-trash://'] = 'trash',
} }
default_config.adapter_aliases = {} default_config.adapter_aliases = {}
-- We want the function in the default config for documentation generation, but if we nil it out -- We want the function in the default config for documentation generation, but if we nil it out
@ -408,7 +408,7 @@ local M = {}
M.setup = function(opts) M.setup = function(opts)
opts = opts or vim.g.oil or {} opts = opts or vim.g.oil or {}
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 {}
elseif opts.keymaps then elseif opts.keymaps then
@ -429,19 +429,19 @@ M.setup = function(opts)
end end
-- Backwards compatibility for old versions that don't support winborder -- Backwards compatibility for old versions that don't support winborder
if vim.fn.has("nvim-0.11") == 0 then if vim.fn.has('nvim-0.11') == 0 then
new_conf = vim.tbl_deep_extend("keep", new_conf, { new_conf = vim.tbl_deep_extend('keep', new_conf, {
float = { border = "rounded" }, float = { border = 'rounded' },
confirmation = { border = "rounded" }, confirmation = { border = 'rounded' },
progress = { border = "rounded" }, progress = { border = 'rounded' },
ssh = { border = "rounded" }, ssh = { border = 'rounded' },
keymaps_help = { border = "rounded" }, keymaps_help = { border = 'rounded' },
}) })
end 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)
end end
-- Backwards compatibility. We renamed the 'preview' config to 'preview_win' -- Backwards compatibility. We renamed the 'preview' config to 'preview_win'
if opts.preview and opts.preview.update_on_cursor_moved ~= nil then if opts.preview and opts.preview.update_on_cursor_moved ~= nil then
@ -452,7 +452,7 @@ M.setup = function(opts)
new_conf.lsp_file_methods.autosave_changes = new_conf.lsp_rename_autosave new_conf.lsp_file_methods.autosave_changes = new_conf.lsp_rename_autosave
new_conf.lsp_rename_autosave = nil new_conf.lsp_rename_autosave = nil
vim.notify_once( vim.notify_once(
"oil config value lsp_rename_autosave has moved to lsp_file_methods.autosave_changes.\nCompatibility will be removed on 2024-09-01.", 'oil config value lsp_rename_autosave has moved to lsp_file_methods.autosave_changes.\nCompatibility will be removed on 2024-09-01.',
vim.log.levels.WARN vim.log.levels.WARN
) )
end end
@ -479,10 +479,10 @@ M.get_adapter_by_scheme = function(scheme)
if not scheme then if not scheme then
return nil return nil
end end
if not vim.endswith(scheme, "://") then if not vim.endswith(scheme, '://') then
local pieces = vim.split(scheme, "://", { plain = true }) local pieces = vim.split(scheme, '://', { plain = true })
if #pieces <= 2 then if #pieces <= 2 then
scheme = pieces[1] .. "://" scheme = pieces[1] .. '://'
else else
error(string.format("Malformed url: '%s'", scheme)) error(string.format("Malformed url: '%s'", scheme))
end end
@ -494,7 +494,7 @@ M.get_adapter_by_scheme = function(scheme)
return nil return nil
end end
local ok local ok
ok, adapter = pcall(require, string.format("oil.adapters.%s", name)) ok, adapter = pcall(require, string.format('oil.adapters.%s', name))
if ok then if ok then
adapter.name = name adapter.name = name
M._adapter_by_scheme[scheme] = adapter M._adapter_by_scheme[scheme] = adapter

View file

@ -1,17 +1,17 @@
local log = require("oil.log") local log = require('oil.log')
local M = {} local M = {}
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
---@type boolean ---@type boolean
M.is_windows = uv.os_uname().version:match("Windows") M.is_windows = uv.os_uname().version:match('Windows')
M.is_mac = uv.os_uname().sysname == "Darwin" M.is_mac = uv.os_uname().sysname == 'Darwin'
M.is_linux = not M.is_windows and not M.is_mac M.is_linux = not M.is_windows and not M.is_mac
---@type string ---@type string
M.sep = M.is_windows and "\\" or "/" M.sep = M.is_windows and '\\' or '/'
---@param ... string ---@param ... string
M.join = function(...) M.join = function(...)
@ -23,15 +23,15 @@ end
---@return boolean ---@return boolean
M.is_absolute = function(dir) M.is_absolute = function(dir)
if M.is_windows then if M.is_windows then
return dir:match("^%a:\\") return dir:match('^%a:\\')
else else
return vim.startswith(dir, "/") return vim.startswith(dir, '/')
end end
end end
M.abspath = function(path) M.abspath = function(path)
if not M.is_absolute(path) then if not M.is_absolute(path) then
path = vim.fn.fnamemodify(path, ":p") path = vim.fn.fnamemodify(path, ':p')
end end
return path return path
end end
@ -40,11 +40,11 @@ end
---@param mode? integer File mode in decimal (default 420 = 0644) ---@param mode? integer File mode in decimal (default 420 = 0644)
---@param cb fun(err: nil|string) ---@param cb fun(err: nil|string)
M.touch = function(path, mode, cb) M.touch = function(path, mode, cb)
if type(mode) == "function" then if type(mode) == 'function' then
cb = mode cb = mode
mode = 420 mode = 420
end end
uv.fs_open(path, "a", mode or 420, function(err, fd) uv.fs_open(path, 'a', mode or 420, function(err, fd)
if err then if err then
cb(err) cb(err)
else else
@ -59,12 +59,12 @@ end
---@param candidate string ---@param candidate string
---@return boolean ---@return boolean
M.is_subpath = function(root, candidate) M.is_subpath = function(root, candidate)
if candidate == "" then if candidate == '' then
return false return false
end end
root = vim.fs.normalize(M.abspath(root)) root = vim.fs.normalize(M.abspath(root))
-- Trim trailing "/" from the root -- Trim trailing "/" from the root
if root:find("/", -1) then if root:find('/', -1) then
root = root:sub(1, -2) root = root:sub(1, -2)
end end
candidate = vim.fs.normalize(M.abspath(candidate)) candidate = vim.fs.normalize(M.abspath(candidate))
@ -80,8 +80,8 @@ M.is_subpath = function(root, candidate)
return false return false
end end
local candidate_starts_with_sep = candidate:find("/", root:len() + 1, true) == root:len() + 1 local candidate_starts_with_sep = candidate:find('/', root:len() + 1, true) == root:len() + 1
local root_ends_with_sep = root:find("/", root:len(), true) == root:len() local root_ends_with_sep = root:find('/', root:len(), true) == root:len()
return candidate_starts_with_sep or root_ends_with_sep return candidate_starts_with_sep or root_ends_with_sep
end end
@ -90,15 +90,15 @@ end
---@return string ---@return string
M.posix_to_os_path = function(path) M.posix_to_os_path = function(path)
if M.is_windows then if M.is_windows then
if vim.startswith(path, "/") then if vim.startswith(path, '/') then
local drive = path:match("^/(%a+)") local drive = path:match('^/(%a+)')
if not drive then if not drive then
return path return path
end end
local rem = path:sub(drive:len() + 2) local rem = path:sub(drive:len() + 2)
return string.format("%s:%s", drive, rem:gsub("/", "\\")) return string.format('%s:%s', drive, rem:gsub('/', '\\'))
else else
local newpath = path:gsub("/", "\\") local newpath = path:gsub('/', '\\')
return newpath return newpath
end end
else else
@ -111,10 +111,10 @@ end
M.os_to_posix_path = function(path) M.os_to_posix_path = function(path)
if M.is_windows then if M.is_windows then
if M.is_absolute(path) then if M.is_absolute(path) then
local drive, rem = path:match("^([^:]+):\\(.*)$") local drive, rem = path:match('^([^:]+):\\(.*)$')
return string.format("/%s/%s", drive:upper(), rem:gsub("\\", "/")) return string.format('/%s/%s', drive:upper(), rem:gsub('\\', '/'))
else else
local newpath = path:gsub("\\", "/") local newpath = path:gsub('\\', '/')
return newpath return newpath
end end
else else
@ -135,16 +135,16 @@ M.shorten_path = function(path, relative_to)
if M.is_subpath(relative_to, path) then if M.is_subpath(relative_to, path) then
local idx = relative_to:len() + 1 local idx = relative_to:len() + 1
-- Trim the dividing slash if it's not included in relative_to -- Trim the dividing slash if it's not included in relative_to
if not vim.endswith(relative_to, "/") and not vim.endswith(relative_to, "\\") then if not vim.endswith(relative_to, '/') and not vim.endswith(relative_to, '\\') then
idx = idx + 1 idx = idx + 1
end end
relpath = path:sub(idx) relpath = path:sub(idx)
if relpath == "" then if relpath == '' then
relpath = "." relpath = '.'
end end
end end
if M.is_subpath(home_dir, path) then if M.is_subpath(home_dir, path) then
local homepath = "~" .. path:sub(home_dir:len() + 1) local homepath = '~' .. path:sub(home_dir:len() + 1)
if not relpath or homepath:len() < relpath:len() then if not relpath or homepath:len() < relpath:len() then
return homepath return homepath
end end
@ -156,13 +156,13 @@ end
---@param mode? integer ---@param mode? integer
M.mkdirp = function(dir, mode) M.mkdirp = function(dir, mode)
mode = mode or 493 mode = mode or 493
local mod = "" local mod = ''
local path = dir local path = dir
while vim.fn.isdirectory(path) == 0 do while vim.fn.isdirectory(path) == 0 do
mod = mod .. ":h" mod = mod .. ':h'
path = vim.fn.fnamemodify(dir, mod) path = vim.fn.fnamemodify(dir, mod)
end end
while mod ~= "" do while mod ~= '' do
mod = mod:sub(3) mod = mod:sub(3)
path = vim.fn.fnamemodify(dir, mod) path = vim.fn.fnamemodify(dir, mod)
uv.fs_mkdir(path, mode) uv.fs_mkdir(path, mode)
@ -209,7 +209,7 @@ end
---@param path string ---@param path string
---@param cb fun(err: nil|string) ---@param cb fun(err: nil|string)
M.recursive_delete = function(entry_type, path, cb) M.recursive_delete = function(entry_type, path, cb)
if entry_type ~= "directory" then if entry_type ~= 'directory' then
return uv.fs_unlink(path, cb) return uv.fs_unlink(path, cb)
end end
---@diagnostic disable-next-line: param-type-mismatch, discard-returns ---@diagnostic disable-next-line: param-type-mismatch, discard-returns
@ -271,13 +271,13 @@ local move_undofile = vim.schedule_wrap(function(src_path, dest_path, copy)
if copy then if copy then
uv.fs_copyfile(src_path, dest_path, function(err) uv.fs_copyfile(src_path, dest_path, function(err)
if err then if err then
log.warn("Error copying undofile %s: %s", undofile, err) log.warn('Error copying undofile %s: %s', undofile, err)
end end
end) end)
else else
uv.fs_rename(undofile, dest_undofile, function(err) uv.fs_rename(undofile, dest_undofile, function(err)
if err then if err then
log.warn("Error moving undofile %s: %s", undofile, err) log.warn('Error moving undofile %s: %s', undofile, err)
end end
end) end)
end end
@ -290,7 +290,7 @@ end)
---@param dest_path string ---@param dest_path string
---@param cb fun(err: nil|string) ---@param cb fun(err: nil|string)
M.recursive_copy = function(entry_type, src_path, dest_path, cb) M.recursive_copy = function(entry_type, src_path, dest_path, cb)
if entry_type == "link" then if entry_type == 'link' then
uv.fs_readlink(src_path, function(link_err, link) uv.fs_readlink(src_path, function(link_err, link)
if link_err then if link_err then
return cb(link_err) return cb(link_err)
@ -300,7 +300,7 @@ M.recursive_copy = function(entry_type, src_path, dest_path, cb)
end) end)
return return
end end
if entry_type ~= "directory" then if entry_type ~= 'directory' then
uv.fs_copyfile(src_path, dest_path, { excl = true }, cb) uv.fs_copyfile(src_path, dest_path, { excl = true }, cb)
move_undofile(src_path, dest_path, true) move_undofile(src_path, dest_path, true)
return return
@ -374,7 +374,7 @@ M.recursive_move = function(entry_type, src_path, dest_path, cb)
end end
end) end)
else else
if entry_type ~= "directory" then if entry_type ~= 'directory' then
move_undofile(src_path, dest_path, false) move_undofile(src_path, dest_path, false)
end end
cb() cb()

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
local actions = require("oil.actions") local actions = require('oil.actions')
local config = require("oil.config") local config = require('oil.config')
local layout = require("oil.layout") local layout = require('oil.layout')
local util = require("oil.util") local util = require('oil.util')
local M = {} local M = {}
---@param rhs string|table|fun() ---@param rhs string|table|fun()
@ -9,14 +9,14 @@ local M = {}
---@return table opts ---@return table opts
---@return string|nil mode ---@return string|nil mode
local function resolve(rhs) local function resolve(rhs)
if type(rhs) == "string" and vim.startswith(rhs, "actions.") then if type(rhs) == 'string' and vim.startswith(rhs, 'actions.') then
local action_name = vim.split(rhs, ".", { plain = true })[2] local action_name = vim.split(rhs, '.', { plain = true })[2]
local action = actions[action_name] local action = actions[action_name]
if not action then if not action then
vim.notify("[oil.nvim] Unknown action name: " .. action_name, vim.log.levels.ERROR) vim.notify('[oil.nvim] Unknown action name: ' .. action_name, vim.log.levels.ERROR)
end end
return resolve(action) return resolve(action)
elseif type(rhs) == "table" then elseif type(rhs) == 'table' then
local opts = vim.deepcopy(rhs) local opts = vim.deepcopy(rhs)
-- We support passing in a `callback` key, or using the 1 index as the rhs of the keymap -- We support passing in a `callback` key, or using the 1 index as the rhs of the keymap
local callback, parent_opts = resolve(opts.callback or opts[1]) local callback, parent_opts = resolve(opts.callback or opts[1])
@ -25,17 +25,17 @@ local function resolve(rhs)
if parent_opts.desc and not opts.desc then if parent_opts.desc and not opts.desc then
if opts.opts then if opts.opts then
opts.desc = opts.desc =
string.format("%s %s", parent_opts.desc, vim.inspect(opts.opts):gsub("%s+", " ")) string.format('%s %s', parent_opts.desc, vim.inspect(opts.opts):gsub('%s+', ' '))
else else
opts.desc = parent_opts.desc opts.desc = parent_opts.desc
end end
end end
local mode = opts.mode local mode = opts.mode
if type(rhs.callback) == "string" then if type(rhs.callback) == 'string' then
local action_opts, action_mode local action_opts, action_mode
callback, action_opts, action_mode = resolve(rhs.callback) callback, action_opts, action_mode = resolve(rhs.callback)
opts = vim.tbl_extend("keep", opts, action_opts) opts = vim.tbl_extend('keep', opts, action_opts)
mode = mode or action_mode mode = mode or action_mode
end end
@ -46,7 +46,7 @@ local function resolve(rhs)
opts.deprecated = nil opts.deprecated = nil
opts.parameters = nil opts.parameters = nil
if opts.opts and type(callback) == "function" then if opts.opts and type(callback) == 'function' then
local callback_args = opts.opts local callback_args = opts.opts
opts.opts = nil opts.opts = nil
local orig_callback = callback local orig_callback = callback
@ -68,7 +68,7 @@ M.set_keymaps = function(keymaps, bufnr)
for k, v in pairs(keymaps) do for k, v in pairs(keymaps) do
local rhs, opts, mode = resolve(v) local rhs, opts, mode = resolve(v)
if rhs then if rhs then
vim.keymap.set(mode or "", k, rhs, vim.tbl_extend("keep", { buffer = bufnr }, opts)) vim.keymap.set(mode or '', k, rhs, vim.tbl_extend('keep', { buffer = bufnr }, opts))
end end
end end
end end
@ -95,9 +95,9 @@ M.show_help = function(keymaps)
local all_lhs = lhs_to_all_lhs[k] local all_lhs = lhs_to_all_lhs[k]
if all_lhs then if all_lhs then
local _, opts = resolve(rhs) local _, opts = resolve(rhs)
local keystr = table.concat(all_lhs, "/") local keystr = table.concat(all_lhs, '/')
max_lhs = math.max(max_lhs, vim.api.nvim_strwidth(keystr)) max_lhs = math.max(max_lhs, vim.api.nvim_strwidth(keystr))
table.insert(keymap_entries, { str = keystr, all_lhs = all_lhs, desc = opts.desc or "" }) table.insert(keymap_entries, { str = keystr, all_lhs = all_lhs, desc = opts.desc or '' })
end end
end end
table.sort(keymap_entries, function(a, b) table.sort(keymap_entries, function(a, b)
@ -108,20 +108,20 @@ 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.pad_align(entry.str, max_lhs, "left"), 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
for _, key in ipairs(entry.all_lhs) do for _, key in ipairs(entry.all_lhs) do
local keywidth = vim.api.nvim_strwidth(key) local keywidth = vim.api.nvim_strwidth(key)
table.insert(highlights, { "Special", #lines, start, start + keywidth }) table.insert(highlights, { 'Special', #lines, start, start + keywidth })
start = start + keywidth + 1 start = start + keywidth + 1
end end
end end
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
local ns = vim.api.nvim_create_namespace("Oil") local ns = vim.api.nvim_create_namespace('Oil')
for _, hl in ipairs(highlights) do for _, hl in ipairs(highlights) do
local hl_group, lnum, start_col, end_col = unpack(hl) local hl_group, lnum, start_col, end_col = unpack(hl)
vim.api.nvim_buf_set_extmark(bufnr, ns, lnum - 1, start_col, { vim.api.nvim_buf_set_extmark(bufnr, ns, lnum - 1, start_col, {
@ -129,21 +129,21 @@ M.show_help = function(keymaps)
hl_group = hl_group, hl_group = hl_group,
}) })
end end
vim.keymap.set("n", "q", "<cmd>close<CR>", { buffer = bufnr }) vim.keymap.set('n', 'q', '<cmd>close<CR>', { buffer = bufnr })
vim.keymap.set("n", "<c-c>", "<cmd>close<CR>", { buffer = bufnr }) vim.keymap.set('n', '<c-c>', '<cmd>close<CR>', { buffer = bufnr })
vim.bo[bufnr].modifiable = false vim.bo[bufnr].modifiable = false
vim.bo[bufnr].bufhidden = "wipe" vim.bo[bufnr].bufhidden = 'wipe'
local editor_width = vim.o.columns local editor_width = vim.o.columns
local editor_height = layout.get_editor_height() local editor_height = layout.get_editor_height()
local winid = vim.api.nvim_open_win(bufnr, true, { local winid = vim.api.nvim_open_win(bufnr, true, {
relative = "editor", relative = 'editor',
row = math.max(0, (editor_height - #lines) / 2), row = math.max(0, (editor_height - #lines) / 2),
col = math.max(0, (editor_width - max_line - 1) / 2), col = math.max(0, (editor_width - max_line - 1) / 2),
width = math.min(editor_width, max_line + 1), width = math.min(editor_width, max_line + 1),
height = math.min(editor_height, #lines), height = math.min(editor_height, #lines),
zindex = 150, zindex = 150,
style = "minimal", style = 'minimal',
border = config.keymaps_help.border, border = config.keymaps_help.border,
}) })
local function close() local function close()
@ -151,13 +151,13 @@ M.show_help = function(keymaps)
vim.api.nvim_win_close(winid, true) vim.api.nvim_win_close(winid, true)
end end
end end
vim.api.nvim_create_autocmd("BufLeave", { vim.api.nvim_create_autocmd('BufLeave', {
callback = close, callback = close,
once = true, once = true,
nested = true, nested = true,
buffer = bufnr, buffer = bufnr,
}) })
vim.api.nvim_create_autocmd("WinLeave", { vim.api.nvim_create_autocmd('WinLeave', {
callback = close, callback = close,
once = true, once = true,
nested = true, nested = true,

View file

@ -43,7 +43,7 @@ local function calc_list(values, max_value, aggregator, limit)
local ret = limit local ret = limit
if not max_value or not values then if not max_value or not values then
return nil return nil
elseif type(values) == "table" then elseif type(values) == 'table' then
for _, v in ipairs(values) do for _, v in ipairs(values) do
ret = aggregator(ret, calc_float(v, max_value)) ret = aggregator(ret, calc_float(v, max_value))
end end
@ -106,12 +106,12 @@ end
---@return vim.api.keyset.win_config ---@return vim.api.keyset.win_config
M.get_fullscreen_win_opts = function() M.get_fullscreen_win_opts = function()
local config = require("oil.config") local config = require('oil.config')
local total_width = M.get_editor_width() local total_width = M.get_editor_width()
local total_height = M.get_editor_height() local total_height = M.get_editor_height()
local width = total_width - 2 * config.float.padding local width = total_width - 2 * config.float.padding
if config.float.border ~= "none" then if config.float.border ~= 'none' then
width = width - 2 -- The border consumes 1 col on each side width = width - 2 -- The border consumes 1 col on each side
end end
if config.float.max_width > 0 then if config.float.max_width > 0 then
@ -127,7 +127,7 @@ M.get_fullscreen_win_opts = function()
local col = math.floor((total_width - width) / 2) - 1 -- adjust for border width local col = math.floor((total_width - width) / 2) - 1 -- adjust for border width
local win_opts = { local win_opts = {
relative = "editor", relative = 'editor',
width = width, width = width,
height = height, height = height,
row = row, row = row,
@ -144,8 +144,8 @@ end
---@return oil.WinLayout root_dim New dimensions of the original window ---@return oil.WinLayout root_dim New dimensions of the original window
---@return oil.WinLayout new_dim New dimensions of the new window ---@return oil.WinLayout new_dim New dimensions of the new window
M.split_window = function(winid, direction, gap) M.split_window = function(winid, direction, gap)
if direction == "auto" then if direction == 'auto' then
direction = vim.o.splitright and "right" or "left" direction = vim.o.splitright and 'right' or 'left'
end end
local float_config = vim.api.nvim_win_get_config(winid) local float_config = vim.api.nvim_win_get_config(winid)
@ -156,14 +156,14 @@ M.split_window = function(winid, direction, gap)
col = float_config.col, col = float_config.col,
row = float_config.row, row = float_config.row,
} }
if vim.fn.has("nvim-0.10") == 0 then if vim.fn.has('nvim-0.10') == 0 then
-- read https://github.com/neovim/neovim/issues/24430 for more infos. -- read https://github.com/neovim/neovim/issues/24430 for more infos.
dim_root.col = float_config.col[vim.val_idx] dim_root.col = float_config.col[vim.val_idx]
dim_root.row = float_config.row[vim.val_idx] dim_root.row = float_config.row[vim.val_idx]
end end
local dim_new = vim.deepcopy(dim_root) local dim_new = vim.deepcopy(dim_root)
if direction == "left" or direction == "right" then if direction == 'left' or direction == 'right' then
dim_new.width = math.floor(float_config.width / 2) - math.ceil(gap / 2) dim_new.width = math.floor(float_config.width / 2) - math.ceil(gap / 2)
dim_root.width = dim_new.width dim_root.width = dim_new.width
else else
@ -171,13 +171,13 @@ M.split_window = function(winid, direction, gap)
dim_root.height = dim_new.height dim_root.height = dim_new.height
end end
if direction == "left" then if direction == 'left' then
dim_root.col = dim_root.col + dim_root.width + gap dim_root.col = dim_root.col + dim_root.width + gap
elseif direction == "right" then elseif direction == 'right' then
dim_new.col = dim_new.col + dim_new.width + gap dim_new.col = dim_new.col + dim_new.width + gap
elseif direction == "above" then elseif direction == 'above' then
dim_root.row = dim_root.row + dim_root.height + gap dim_root.row = dim_root.row + dim_root.height + gap
elseif direction == "below" then elseif direction == 'below' then
dim_new.row = dim_new.row + dim_new.height + gap dim_new.row = dim_new.row + dim_new.height + gap
end end

View file

@ -1,4 +1,4 @@
local util = require("oil.util") local util = require('oil.util')
local M = {} local M = {}
local timers = {} local timers = {}
@ -12,14 +12,14 @@ M.is_loading = function(bufnr)
end end
local spinners = { local spinners = {
dots = { "", "", "", "", "", "", "", "", "", "" }, dots = { '', '', '', '', '', '', '', '', '', '' },
} }
---@param name_or_frames string|string[] ---@param name_or_frames string|string[]
---@return fun(): string ---@return fun(): string
M.get_iter = function(name_or_frames) M.get_iter = function(name_or_frames)
local frames local frames
if type(name_or_frames) == "string" then if type(name_or_frames) == 'string' then
frames = spinners[name_or_frames] frames = spinners[name_or_frames]
if not frames then if not frames then
error(string.format("Unrecognized spinner: '%s'", name_or_frames)) error(string.format("Unrecognized spinner: '%s'", name_or_frames))
@ -35,26 +35,26 @@ M.get_iter = function(name_or_frames)
end end
M.get_bar_iter = function(opts) M.get_bar_iter = function(opts)
opts = vim.tbl_deep_extend("keep", opts or {}, { opts = vim.tbl_deep_extend('keep', opts or {}, {
bar_size = 3, bar_size = 3,
width = 20, width = 20,
}) })
local i = 0 local i = 0
return function() return function()
local chars = { "[" } local chars = { '[' }
for _ = 1, opts.width - 2 do for _ = 1, opts.width - 2 do
table.insert(chars, " ") table.insert(chars, ' ')
end end
table.insert(chars, "]") table.insert(chars, ']')
for j = i - opts.bar_size, i do for j = i - opts.bar_size, i do
if j > 1 and j < opts.width then if j > 1 and j < opts.width then
chars[j] = "=" chars[j] = '='
end end
end end
i = (i + 1) % (opts.width + opts.bar_size) i = (i + 1) % (opts.width + opts.bar_size)
return table.concat(chars, "") return table.concat(chars, '')
end end
end end
@ -75,7 +75,7 @@ M.set_loading = function(bufnr, is_loading)
return return
end end
local lines = local lines =
{ util.pad_align("Loading", math.floor(width / 2) - 3, "right"), bar_iter() } { 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

@ -11,14 +11,14 @@ Log.level = vim.log.levels.WARN
---@return string ---@return string
Log.get_logfile = function() Log.get_logfile = function()
local fs = require("oil.fs") local fs = require('oil.fs')
local ok, stdpath = pcall(vim.fn.stdpath, "log") local ok, stdpath = pcall(vim.fn.stdpath, 'log')
if not ok then if not ok then
stdpath = vim.fn.stdpath("cache") stdpath = vim.fn.stdpath('cache')
end end
assert(type(stdpath) == "string") assert(type(stdpath) == 'string')
return fs.join(stdpath, "oil.log") return fs.join(stdpath, 'oil.log')
end end
---@param level integer ---@param level integer
@ -29,19 +29,19 @@ local function format(level, msg, ...)
local args = vim.F.pack_len(...) local args = vim.F.pack_len(...)
for i = 1, args.n do for i = 1, args.n do
local v = args[i] local v = args[i]
if type(v) == "table" then if type(v) == 'table' then
args[i] = vim.inspect(v) args[i] = vim.inspect(v)
elseif v == nil then elseif v == nil then
args[i] = "nil" args[i] = 'nil'
end end
end end
local ok, text = pcall(string.format, msg, vim.F.unpack_len(args)) local ok, text = pcall(string.format, msg, vim.F.unpack_len(args))
-- TODO figure out how to get formatted time inside luv callback -- TODO figure out how to get formatted time inside luv callback
-- local timestr = vim.fn.strftime("%Y-%m-%d %H:%M:%S") -- local timestr = vim.fn.strftime("%Y-%m-%d %H:%M:%S")
local timestr = "" local timestr = ''
if ok then if ok then
local str_level = levels_reverse[level] local str_level = levels_reverse[level]
return string.format("%s[%s] %s", timestr, str_level, text) return string.format('%s[%s] %s', timestr, str_level, text)
else else
return string.format( return string.format(
"%s[ERROR] error formatting log line: '%s' args %s", "%s[ERROR] error formatting log line: '%s' args %s",
@ -67,22 +67,22 @@ local function initialize()
local stat = uv.fs_stat(filepath) local stat = uv.fs_stat(filepath)
if stat and stat.size > 10 * 1024 * 1024 then if stat and stat.size > 10 * 1024 * 1024 then
local backup = filepath .. ".1" local backup = filepath .. '.1'
uv.fs_unlink(backup) uv.fs_unlink(backup)
uv.fs_rename(filepath, backup) uv.fs_rename(filepath, backup)
end end
local parent = vim.fs.dirname(filepath) local parent = vim.fs.dirname(filepath)
require("oil.fs").mkdirp(parent) require('oil.fs').mkdirp(parent)
local logfile, openerr = io.open(filepath, "a+") local logfile, openerr = io.open(filepath, 'a+')
if not logfile then if not logfile then
local err_msg = string.format("Failed to open oil.nvim log file: %s", openerr) local err_msg = string.format('Failed to open oil.nvim log file: %s', openerr)
vim.notify(err_msg, vim.log.levels.ERROR) vim.notify(err_msg, vim.log.levels.ERROR)
else else
write = function(line) write = function(line)
logfile:write(line) logfile:write(line)
logfile:write("\n") logfile:write('\n')
logfile:flush() logfile:flush()
end end
end end

View file

@ -1,7 +1,7 @@
local config = require("oil.config") local config = require('oil.config')
local fs = require("oil.fs") local fs = require('oil.fs')
local util = require("oil.util") local util = require('oil.util')
local workspace = require("oil.lsp.workspace") local workspace = require('oil.lsp.workspace')
local M = {} local M = {}
@ -12,7 +12,7 @@ M.will_perform_file_operations = function(actions)
local creates = {} local creates = {}
local deletes = {} local deletes = {}
for _, action in ipairs(actions) do for _, action in ipairs(actions) do
if action.type == "move" then if action.type == 'move' then
local src_scheme, src_path = util.parse_url(action.src_url) local src_scheme, src_path = util.parse_url(action.src_url)
assert(src_path) assert(src_path)
local src_adapter = assert(config.get_adapter_by_scheme(src_scheme)) local src_adapter = assert(config.get_adapter_by_scheme(src_scheme))
@ -20,32 +20,32 @@ M.will_perform_file_operations = function(actions)
local dest_adapter = assert(config.get_adapter_by_scheme(dest_scheme)) local dest_adapter = assert(config.get_adapter_by_scheme(dest_scheme))
src_path = fs.posix_to_os_path(src_path) src_path = fs.posix_to_os_path(src_path)
dest_path = fs.posix_to_os_path(assert(dest_path)) dest_path = fs.posix_to_os_path(assert(dest_path))
if src_adapter.name == "files" and dest_adapter.name == "files" then if src_adapter.name == 'files' and dest_adapter.name == 'files' then
moves[src_path] = dest_path moves[src_path] = dest_path
elseif src_adapter.name == "files" then elseif src_adapter.name == 'files' then
table.insert(deletes, src_path) table.insert(deletes, src_path)
elseif dest_adapter.name == "files" then elseif dest_adapter.name == 'files' then
table.insert(creates, src_path) table.insert(creates, src_path)
end end
elseif action.type == "create" then elseif action.type == 'create' then
local scheme, path = util.parse_url(action.url) local scheme, path = util.parse_url(action.url)
path = fs.posix_to_os_path(assert(path)) path = fs.posix_to_os_path(assert(path))
local adapter = assert(config.get_adapter_by_scheme(scheme)) local adapter = assert(config.get_adapter_by_scheme(scheme))
if adapter.name == "files" then if adapter.name == 'files' then
table.insert(creates, path) table.insert(creates, path)
end end
elseif action.type == "delete" then elseif action.type == 'delete' then
local scheme, path = util.parse_url(action.url) local scheme, path = util.parse_url(action.url)
path = fs.posix_to_os_path(assert(path)) path = fs.posix_to_os_path(assert(path))
local adapter = assert(config.get_adapter_by_scheme(scheme)) local adapter = assert(config.get_adapter_by_scheme(scheme))
if adapter.name == "files" then if adapter.name == 'files' then
table.insert(deletes, path) table.insert(deletes, path)
end end
elseif action.type == "copy" then elseif action.type == 'copy' then
local scheme, path = util.parse_url(action.dest_url) local scheme, path = util.parse_url(action.dest_url)
path = fs.posix_to_os_path(assert(path)) path = fs.posix_to_os_path(assert(path))
local adapter = assert(config.get_adapter_by_scheme(scheme)) local adapter = assert(config.get_adapter_by_scheme(scheme))
if adapter.name == "files" then if adapter.name == 'files' then
table.insert(creates, path) table.insert(creates, path)
end end
end end
@ -84,7 +84,7 @@ M.will_perform_file_operations = function(actions)
accum(workspace.will_rename_files(moves, { timeout_ms = timeout_ms })) accum(workspace.will_rename_files(moves, { timeout_ms = timeout_ms }))
if final_err then if final_err then
vim.notify( vim.notify(
string.format("[lsp] file operation error: %s", vim.inspect(final_err)), string.format('[lsp] file operation error: %s', vim.inspect(final_err)),
vim.log.levels.WARN vim.log.levels.WARN
) )
end end
@ -102,7 +102,7 @@ M.will_perform_file_operations = function(actions)
local bufnr = vim.uri_to_bufnr(uri) local bufnr = vim.uri_to_bufnr(uri)
local was_open = buf_was_modified[bufnr] ~= nil local was_open = buf_was_modified[bufnr] ~= nil
local was_modified = buf_was_modified[bufnr] local was_modified = buf_was_modified[bufnr]
local should_save = autosave == true or (autosave == "unmodified" and not was_modified) local should_save = autosave == true or (autosave == 'unmodified' and not was_modified)
-- Autosave changed buffers if they were not modified before -- Autosave changed buffers if they were not modified before
if should_save then if should_save then
vim.api.nvim_buf_call(bufnr, function() vim.api.nvim_buf_call(bufnr, function()

View file

@ -1,13 +1,13 @@
local fs = require("oil.fs") local fs = require('oil.fs')
local ms = require("vim.lsp.protocol").Methods local ms = require('vim.lsp.protocol').Methods
if vim.fn.has("nvim-0.10") == 0 then if vim.fn.has('nvim-0.10') == 0 then
ms = { ms = {
workspace_willCreateFiles = "workspace/willCreateFiles", workspace_willCreateFiles = 'workspace/willCreateFiles',
workspace_didCreateFiles = "workspace/didCreateFiles", workspace_didCreateFiles = 'workspace/didCreateFiles',
workspace_willDeleteFiles = "workspace/willDeleteFiles", workspace_willDeleteFiles = 'workspace/willDeleteFiles',
workspace_didDeleteFiles = "workspace/didDeleteFiles", workspace_didDeleteFiles = 'workspace/didDeleteFiles',
workspace_willRenameFiles = "workspace/willRenameFiles", workspace_willRenameFiles = 'workspace/willRenameFiles',
workspace_didRenameFiles = "workspace/didRenameFiles", workspace_didRenameFiles = 'workspace/didRenameFiles',
} }
end end
@ -16,7 +16,7 @@ local M = {}
---@param method string ---@param method string
---@return vim.lsp.Client[] ---@return vim.lsp.Client[]
local function get_clients(method) local function get_clients(method)
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has('nvim-0.10') == 1 then
return vim.lsp.get_clients({ method = method }) return vim.lsp.get_clients({ method = method })
else else
---@diagnostic disable-next-line: deprecated ---@diagnostic disable-next-line: deprecated
@ -32,7 +32,7 @@ end
---@return boolean ---@return boolean
local function match_glob(glob, path) local function match_glob(glob, path)
-- nvim-0.10 will have vim.glob.to_lpeg, so this will be a LPeg pattern -- nvim-0.10 will have vim.glob.to_lpeg, so this will be a LPeg pattern
if type(glob) ~= "string" then if type(glob) ~= 'string' then
return glob:match(path) ~= nil return glob:match(path) ~= nil
end end
@ -59,7 +59,7 @@ local function get_matching_paths(client, filters, paths)
local match_fns = {} local match_fns = {}
for _, filter in ipairs(filters) do for _, filter in ipairs(filters) do
if filter.scheme == nil or filter.scheme == "file" then if filter.scheme == nil or filter.scheme == 'file' then
local pattern = filter.pattern local pattern = filter.pattern
local glob = pattern.glob local glob = pattern.glob
local ignore_case = pattern.options and pattern.options.ignoreCase local ignore_case = pattern.options and pattern.options.ignoreCase
@ -69,32 +69,32 @@ local function get_matching_paths(client, filters, paths)
-- Some language servers use forward slashes as path separators on Windows (LuaLS) -- Some language servers use forward slashes as path separators on Windows (LuaLS)
-- We no longer need this after 0.12: https://github.com/neovim/neovim/commit/322a6d305d088420b23071c227af07b7c1beb41a -- We no longer need this after 0.12: https://github.com/neovim/neovim/commit/322a6d305d088420b23071c227af07b7c1beb41a
if vim.fn.has("nvim-0.12") == 0 and fs.is_windows then if 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) glob = glob:gsub('{(.-)}', function(s)
local patterns = vim.split(s, ",") local patterns = vim.split(s, ',')
local filtered = {} local filtered = {}
for _, pat in ipairs(patterns) do for _, pat in ipairs(patterns) do
if pat ~= "" then if pat ~= '' then
table.insert(filtered, pat) table.insert(filtered, pat)
end end
end end
if #filtered == 0 then if #filtered == 0 then
return "" return ''
end 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
table.sort(filtered, function(a, b) table.sort(filtered, function(a, b)
return a:len() > b:len() return a:len() > b:len()
end) end)
end end
return "{" .. table.concat(filtered, ",") .. "}" return '{' .. table.concat(filtered, ',') .. '}'
end) end)
glob_to_match = vim.glob.to_lpeg(glob) glob_to_match = vim.glob.to_lpeg(glob)
@ -102,7 +102,7 @@ local function get_matching_paths(client, filters, paths)
local matches = pattern.matches local matches = pattern.matches
table.insert(match_fns, function(path) table.insert(match_fns, function(path)
local is_dir = vim.fn.isdirectory(path) == 1 local is_dir = vim.fn.isdirectory(path) == 1
if matches and ((matches == "file" and is_dir) or (matches == "folder" and not is_dir)) then if matches and ((matches == 'file' and is_dir) or (matches == 'folder' and not is_dir)) then
return false return false
end end
@ -163,10 +163,10 @@ local function will_file_operation(method, capability_name, files, options)
for _, client in ipairs(clients) do for _, client in ipairs(clients) do
local filters = vim.tbl_get( local filters = vim.tbl_get(
client.server_capabilities, client.server_capabilities,
"workspace", 'workspace',
"fileOperations", 'fileOperations',
capability_name, capability_name,
"filters" 'filters'
) )
local matching_files = get_matching_paths(client, filters, files) local matching_files = get_matching_paths(client, filters, files)
if matching_files then if matching_files then
@ -178,7 +178,7 @@ local function will_file_operation(method, capability_name, files, options)
end, matching_files), end, matching_files),
} }
local result, err local result, err
if vim.fn.has("nvim-0.11") == 1 then if vim.fn.has('nvim-0.11') == 1 then
result, err = client:request_sync(method, params, options.timeout_ms or 1000, 0) result, err = client:request_sync(method, params, options.timeout_ms or 1000, 0)
else else
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch
@ -205,10 +205,10 @@ local function did_file_operation(method, capability_name, files)
for _, client in ipairs(clients) do for _, client in ipairs(clients) do
local filters = vim.tbl_get( local filters = vim.tbl_get(
client.server_capabilities, client.server_capabilities,
"workspace", 'workspace',
"fileOperations", 'fileOperations',
capability_name, capability_name,
"filters" 'filters'
) )
local matching_files = get_matching_paths(client, filters, files) local matching_files = get_matching_paths(client, filters, files)
if matching_files then if matching_files then
@ -219,7 +219,7 @@ local function did_file_operation(method, capability_name, files)
} }
end, matching_files), end, matching_files),
} }
if vim.fn.has("nvim-0.11") == 1 then if vim.fn.has('nvim-0.11') == 1 then
client:notify(method, params) client:notify(method, params)
else else
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch
@ -239,13 +239,13 @@ end
---@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[] ---@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[]
---@return nil|string|lsp.ResponseError err ---@return nil|string|lsp.ResponseError err
function M.will_create_files(files, options) function M.will_create_files(files, options)
return will_file_operation(ms.workspace_willCreateFiles, "willCreate", files, options) return will_file_operation(ms.workspace_willCreateFiles, 'willCreate', files, options)
end end
--- Notify the server that files were created from within the client. --- Notify the server that files were created from within the client.
---@param files string[] The files and folders that will be created ---@param files string[] The files and folders that will be created
function M.did_create_files(files) function M.did_create_files(files)
did_file_operation(ms.workspace_didCreateFiles, "didCreate", files) did_file_operation(ms.workspace_didCreateFiles, 'didCreate', files)
end end
--- Notify the server that the client is about to delete files. --- Notify the server that the client is about to delete files.
@ -258,13 +258,13 @@ end
---@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[] ---@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[]
---@return nil|string|lsp.ResponseError err ---@return nil|string|lsp.ResponseError err
function M.will_delete_files(files, options) function M.will_delete_files(files, options)
return will_file_operation(ms.workspace_willDeleteFiles, "willDelete", files, options) return will_file_operation(ms.workspace_willDeleteFiles, 'willDelete', files, options)
end end
--- Notify the server that files were deleted from within the client. --- Notify the server that files were deleted from within the client.
---@param files string[] The files and folders that were deleted ---@param files string[] The files and folders that were deleted
function M.did_delete_files(files) function M.did_delete_files(files)
did_file_operation(ms.workspace_didDeleteFiles, "didDelete", files) did_file_operation(ms.workspace_didDeleteFiles, 'didDelete', files)
end end
--- Notify the server that the client is about to rename files. --- Notify the server that the client is about to rename files.
@ -284,10 +284,10 @@ function M.will_rename_files(files, options)
for _, client in ipairs(clients) do for _, client in ipairs(clients) do
local filters = vim.tbl_get( local filters = vim.tbl_get(
client.server_capabilities, client.server_capabilities,
"workspace", 'workspace',
"fileOperations", 'fileOperations',
"willRename", 'willRename',
"filters" 'filters'
) )
local matching_files = get_matching_paths(client, filters, vim.tbl_keys(files)) local matching_files = get_matching_paths(client, filters, vim.tbl_keys(files))
if matching_files then if matching_files then
@ -300,7 +300,7 @@ function M.will_rename_files(files, options)
end, matching_files), end, matching_files),
} }
local result, err local result, err
if vim.fn.has("nvim-0.11") == 1 then if vim.fn.has('nvim-0.11') == 1 then
result, err = result, err =
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)
else else
@ -327,7 +327,7 @@ function M.did_rename_files(files)
local clients = get_clients(ms.workspace_didRenameFiles) local clients = get_clients(ms.workspace_didRenameFiles)
for _, client in ipairs(clients) do for _, client in ipairs(clients) do
local filters = local filters =
vim.tbl_get(client.server_capabilities, "workspace", "fileOperations", "didRename", "filters") vim.tbl_get(client.server_capabilities, 'workspace', 'fileOperations', 'didRename', 'filters')
local matching_files = get_matching_paths(client, filters, vim.tbl_keys(files)) local matching_files = get_matching_paths(client, filters, vim.tbl_keys(files))
if matching_files then if matching_files then
local params = { local params = {
@ -338,7 +338,7 @@ function M.did_rename_files(files)
} }
end, matching_files), end, matching_files),
} }
if vim.fn.has("nvim-0.11") == 1 then if vim.fn.has('nvim-0.11') == 1 then
client:notify(ms.workspace_didRenameFiles, params) client:notify(ms.workspace_didRenameFiles, params)
else else
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch

View file

@ -1,7 +1,7 @@
local columns = require("oil.columns") local columns = require('oil.columns')
local config = require("oil.config") local config = require('oil.config')
local layout = require("oil.layout") local layout = require('oil.layout')
local util = require("oil.util") local util = require('oil.util')
local M = {} local M = {}
---@param actions oil.Action[] ---@param actions oil.Action[]
@ -12,17 +12,17 @@ local function is_simple_edit(actions)
local num_move = 0 local num_move = 0
for _, action in ipairs(actions) do for _, action in ipairs(actions) do
-- If there are any deletes, it is not a simple edit -- If there are any deletes, it is not a simple edit
if action.type == "delete" then if action.type == 'delete' then
return false return false
elseif action.type == "create" then elseif action.type == 'create' then
num_create = num_create + 1 num_create = num_create + 1
elseif action.type == "copy" then elseif action.type == 'copy' then
num_copy = num_copy + 1 num_copy = num_copy + 1
-- Cross-adapter copies are not simple -- Cross-adapter copies are not simple
if util.parse_url(action.src_url) ~= util.parse_url(action.dest_url) then if util.parse_url(action.src_url) ~= util.parse_url(action.dest_url) then
return false return false
end end
elseif action.type == "move" then elseif action.type == 'move' then
num_move = num_move + 1 num_move = num_move + 1
-- Cross-adapter moves are not simple -- Cross-adapter moves are not simple
if util.parse_url(action.src_url) ~= util.parse_url(action.dest_url) then if util.parse_url(action.src_url) ~= util.parse_url(action.dest_url) then
@ -46,10 +46,10 @@ end
---@param lines string[] ---@param lines string[]
local function render_lines(winid, bufnr, lines) local function render_lines(winid, bufnr, lines)
util.render_text(bufnr, lines, { util.render_text(bufnr, lines, {
v_align = "top", v_align = 'top',
h_align = "left", h_align = 'left',
winid = winid, winid = winid,
actions = { "[Y]es", "[N]o" }, actions = { '[Y]es', '[N]o' },
}) })
end end
@ -70,48 +70,48 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
-- Create the buffer -- Create the buffer
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].bufhidden = "wipe" vim.bo[bufnr].bufhidden = 'wipe'
local lines = {} local lines = {}
local max_line_width = 0 local max_line_width = 0
for _, action in ipairs(actions) do for _, action in ipairs(actions) do
local adapter = util.get_adapter_for_action(action) local adapter = util.get_adapter_for_action(action)
local line local line
if action.type == "change" then if action.type == 'change' then
---@cast action oil.ChangeAction ---@cast action oil.ChangeAction
line = columns.render_change_action(adapter, action) line = columns.render_change_action(adapter, action)
else else
line = adapter.render_action(action) line = adapter.render_action(action)
end end
-- We can't handle lines with newlines in them -- We can't handle lines with newlines in them
line = line:gsub("\n", "") line = line:gsub('\n', '')
table.insert(lines, line) table.insert(lines, line)
local line_width = vim.api.nvim_strwidth(line) local line_width = vim.api.nvim_strwidth(line)
if line_width > max_line_width then if line_width > max_line_width then
max_line_width = line_width max_line_width = line_width
end end
end end
table.insert(lines, "") table.insert(lines, '')
-- Create the floating window -- Create the floating window
local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.confirmation) local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.confirmation)
local ok, winid = pcall(vim.api.nvim_open_win, bufnr, true, { local ok, winid = pcall(vim.api.nvim_open_win, bufnr, true, {
relative = "editor", relative = 'editor',
width = width, width = width,
height = height, height = height,
row = math.floor((layout.get_editor_height() - height) / 2), row = math.floor((layout.get_editor_height() - height) / 2),
col = math.floor((layout.get_editor_width() - width) / 2), col = math.floor((layout.get_editor_width() - width) / 2),
zindex = 152, -- render on top of the floating window title zindex = 152, -- render on top of the floating window title
style = "minimal", style = 'minimal',
border = config.confirmation.border, border = config.confirmation.border,
}) })
if not ok then if not ok then
vim.notify(string.format("Error showing oil preview window: %s", winid), vim.log.levels.ERROR) vim.notify(string.format('Error showing oil preview window: %s', winid), vim.log.levels.ERROR)
cb(false) cb(false)
end end
vim.bo[bufnr].filetype = "oil_preview" vim.bo[bufnr].filetype = 'oil_preview'
vim.bo[bufnr].syntax = "oil_preview" vim.bo[bufnr].syntax = 'oil_preview'
for k, v in pairs(config.confirmation.win_options) do for k, v in pairs(config.confirmation.win_options) do
vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) vim.api.nvim_set_option_value(k, v, { scope = 'local', win = winid })
end end
render_lines(winid, bufnr, lines) render_lines(winid, bufnr, lines)
@ -137,7 +137,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
end end
cancel = make_callback(false) cancel = make_callback(false)
confirm = make_callback(true) confirm = make_callback(true)
vim.api.nvim_create_autocmd("BufLeave", { vim.api.nvim_create_autocmd('BufLeave', {
callback = function() callback = function()
cancel() cancel()
end, end,
@ -145,7 +145,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
nested = true, nested = true,
buffer = bufnr, buffer = bufnr,
}) })
vim.api.nvim_create_autocmd("WinLeave", { vim.api.nvim_create_autocmd('WinLeave', {
callback = function() callback = function()
cancel() cancel()
end, end,
@ -154,12 +154,12 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
}) })
table.insert( table.insert(
autocmds, autocmds,
vim.api.nvim_create_autocmd("VimResized", { vim.api.nvim_create_autocmd('VimResized', {
callback = function() callback = function()
if vim.api.nvim_win_is_valid(winid) then if vim.api.nvim_win_is_valid(winid) then
width, height = layout.calculate_dims(max_line_width, #lines, config.confirmation) width, height = layout.calculate_dims(max_line_width, #lines, config.confirmation)
vim.api.nvim_win_set_config(winid, { vim.api.nvim_win_set_config(winid, {
relative = "editor", relative = 'editor',
width = width, width = width,
height = height, height = height,
row = math.floor((layout.get_editor_height() - height) / 2), row = math.floor((layout.get_editor_height() - height) / 2),
@ -173,17 +173,17 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
) )
-- We used to use [C]ancel to cancel, so preserve the old keymap -- We used to use [C]ancel to cancel, so preserve the old keymap
local cancel_keys = { "n", "N", "c", "C", "q", "<C-c>", "<Esc>" } local cancel_keys = { 'n', 'N', 'c', 'C', 'q', '<C-c>', '<Esc>' }
for _, cancel_key in ipairs(cancel_keys) do for _, cancel_key in ipairs(cancel_keys) do
vim.keymap.set("n", cancel_key, function() vim.keymap.set('n', cancel_key, function()
cancel() cancel()
end, { buffer = bufnr, nowait = true }) end, { buffer = bufnr, nowait = true })
end end
-- We used to use [O]k to confirm, so preserve the old keymap -- We used to use [O]k to confirm, so preserve the old keymap
local confirm_keys = { "y", "Y", "o", "O" } local confirm_keys = { 'y', 'Y', 'o', 'O' }
for _, confirm_key in ipairs(confirm_keys) do for _, confirm_key in ipairs(confirm_keys) do
vim.keymap.set("n", confirm_key, function() vim.keymap.set('n', confirm_key, function()
confirm() confirm()
end, { buffer = bufnr, nowait = true }) end, { buffer = bufnr, nowait = true })
end end

View file

@ -1,16 +1,16 @@
local Progress = require("oil.mutator.progress") local Progress = require('oil.mutator.progress')
local Trie = require("oil.mutator.trie") local Trie = require('oil.mutator.trie')
local cache = require("oil.cache") local cache = require('oil.cache')
local columns = require("oil.columns") local columns = require('oil.columns')
local config = require("oil.config") local config = require('oil.config')
local confirmation = require("oil.mutator.confirmation") local confirmation = require('oil.mutator.confirmation')
local constants = require("oil.constants") local constants = require('oil.constants')
local fs = require("oil.fs") local fs = require('oil.fs')
local lsp_helpers = require("oil.lsp.helpers") local lsp_helpers = require('oil.lsp.helpers')
local oil = require("oil") local oil = require('oil')
local parser = require("oil.mutator.parser") local parser = require('oil.mutator.parser')
local util = require("oil.util") local util = require('oil.util')
local view = require("oil.view") local view = require('oil.view')
local M = {} local M = {}
local FIELD_NAME = constants.FIELD_NAME local FIELD_NAME = constants.FIELD_NAME
@ -73,7 +73,7 @@ M.create_actions_from_diffs = function(all_diffs)
local function add_action(action) local function add_action(action)
local adapter = assert(config.get_adapter_by_scheme(action.dest_url or action.url)) local adapter = assert(config.get_adapter_by_scheme(action.dest_url or action.url))
if not adapter.filter_action or adapter.filter_action(action) then if not adapter.filter_action or adapter.filter_action(action) then
if action.type == "create" then if action.type == 'create' then
if seen_creates[action.url] then if seen_creates[action.url] then
return return
else else
@ -87,11 +87,11 @@ M.create_actions_from_diffs = function(all_diffs)
for bufnr, diffs in pairs(all_diffs) do for bufnr, diffs in pairs(all_diffs) do
local adapter = util.get_adapter(bufnr, true) local adapter = util.get_adapter(bufnr, true)
if not adapter then if not adapter then
error("Missing adapter") error('Missing adapter')
end end
local parent_url = vim.api.nvim_buf_get_name(bufnr) local parent_url = vim.api.nvim_buf_get_name(bufnr)
for _, diff in ipairs(diffs) do for _, diff in ipairs(diffs) do
if diff.type == "new" then if diff.type == 'new' then
if diff.id then if diff.id then
local by_id = diff_by_id[diff.id] local by_id = diff_by_id[diff.id]
---HACK: set the destination on this diff for use later ---HACK: set the destination on this diff for use later
@ -100,28 +100,28 @@ M.create_actions_from_diffs = function(all_diffs)
table.insert(by_id, diff) table.insert(by_id, diff)
else else
-- Parse nested files like foo/bar/baz -- Parse nested files like foo/bar/baz
local path_sep = fs.is_windows and "[/\\]" or "/" local path_sep = fs.is_windows and '[/\\]' or '/'
local pieces = vim.split(diff.name, path_sep) local pieces = vim.split(diff.name, path_sep)
local url = parent_url:gsub("/$", "") local url = parent_url:gsub('/$', '')
for i, v in ipairs(pieces) do for i, v in ipairs(pieces) do
local is_last = i == #pieces local is_last = i == #pieces
local entry_type = is_last and diff.entry_type or "directory" local entry_type = is_last and diff.entry_type or 'directory'
local alternation = v:match("{([^}]+)}") local alternation = v:match('{([^}]+)}')
if is_last and alternation then if is_last and alternation then
-- Parse alternations like foo.{js,test.js} -- Parse alternations like foo.{js,test.js}
for _, alt in ipairs(vim.split(alternation, ",")) do for _, alt in ipairs(vim.split(alternation, ',')) do
local alt_url = url .. "/" .. v:gsub("{[^}]+}", alt) local alt_url = url .. '/' .. v:gsub('{[^}]+}', alt)
add_action({ add_action({
type = "create", type = 'create',
url = alt_url, url = alt_url,
entry_type = entry_type, entry_type = entry_type,
link = diff.link, link = diff.link,
}) })
end end
else else
url = url .. "/" .. v url = url .. '/' .. v
add_action({ add_action({
type = "create", type = 'create',
url = url, url = url,
entry_type = entry_type, entry_type = entry_type,
link = diff.link, link = diff.link,
@ -129,9 +129,9 @@ M.create_actions_from_diffs = function(all_diffs)
end end
end end
end end
elseif diff.type == "change" then elseif diff.type == 'change' then
add_action({ add_action({
type = "change", type = 'change',
url = parent_url .. diff.name, url = parent_url .. diff.name,
entry_type = diff.entry_type, entry_type = diff.entry_type,
column = diff.column, column = diff.column,
@ -151,7 +151,7 @@ M.create_actions_from_diffs = function(all_diffs)
for id, diffs in pairs(diff_by_id) do for id, diffs in pairs(diff_by_id) do
local entry = cache.get_entry_by_id(id) local entry = cache.get_entry_by_id(id)
if not entry then if not entry then
error(string.format("Could not find entry %d", id)) error(string.format('Could not find entry %d', id))
end end
---HACK: access the has_delete field on the list-like table of diffs ---HACK: access the has_delete field on the list-like table of diffs
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
@ -161,7 +161,7 @@ M.create_actions_from_diffs = function(all_diffs)
-- MOVE (+ optional copies) when has both creates and delete -- MOVE (+ optional copies) when has both creates and delete
for i, diff in ipairs(diffs) do for i, diff in ipairs(diffs) do
add_action({ add_action({
type = i == #diffs and "move" or "copy", type = i == #diffs and 'move' or 'copy',
entry_type = entry[FIELD_TYPE], entry_type = entry[FIELD_TYPE],
---HACK: access the dest field we set above ---HACK: access the dest field we set above
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
@ -172,7 +172,7 @@ M.create_actions_from_diffs = function(all_diffs)
else else
-- DELETE when no create -- DELETE when no create
add_action({ add_action({
type = "delete", type = 'delete',
entry_type = entry[FIELD_TYPE], entry_type = entry[FIELD_TYPE],
url = cache.get_parent_url(id) .. entry[FIELD_NAME], url = cache.get_parent_url(id) .. entry[FIELD_NAME],
}) })
@ -181,7 +181,7 @@ M.create_actions_from_diffs = function(all_diffs)
-- COPY when create but no delete -- COPY when create but no delete
for _, diff in ipairs(diffs) do for _, diff in ipairs(diffs) do
add_action({ add_action({
type = "copy", type = 'copy',
entry_type = entry[FIELD_TYPE], entry_type = entry[FIELD_TYPE],
src_url = cache.get_parent_url(id) .. entry[FIELD_NAME], src_url = cache.get_parent_url(id) .. entry[FIELD_NAME],
---HACK: access the dest field we set above ---HACK: access the dest field we set above
@ -201,9 +201,9 @@ M.enforce_action_order = function(actions)
local src_trie = Trie.new() local src_trie = Trie.new()
local dest_trie = Trie.new() local dest_trie = Trie.new()
for _, action in ipairs(actions) do for _, action in ipairs(actions) do
if action.type == "delete" or action.type == "change" then if action.type == 'delete' or action.type == 'change' then
src_trie:insert_action(action.url, action) src_trie:insert_action(action.url, action)
elseif action.type == "create" then elseif action.type == 'create' then
dest_trie:insert_action(action.url, action) dest_trie:insert_action(action.url, action)
else else
dest_trie:insert_action(action.dest_url, action) dest_trie:insert_action(action.dest_url, action)
@ -223,18 +223,18 @@ M.enforce_action_order = function(actions)
---@param action oil.Action ---@param action oil.Action
local function get_deps(action) local function get_deps(action)
local ret = {} local ret = {}
if action.type == "delete" then if action.type == 'delete' then
src_trie:accum_children_of(action.url, ret) src_trie:accum_children_of(action.url, ret)
elseif action.type == "create" then elseif action.type == 'create' then
-- Finish operating on parents first -- Finish operating on parents first
-- e.g. NEW /a BEFORE NEW /a/b -- e.g. NEW /a BEFORE NEW /a/b
dest_trie:accum_first_parents_of(action.url, ret) dest_trie:accum_first_parents_of(action.url, ret)
-- Process remove path before creating new path -- Process remove path before creating new path
-- e.g. DELETE /a BEFORE NEW /a -- e.g. DELETE /a BEFORE NEW /a
src_trie:accum_actions_at(action.url, ret, function(a) src_trie:accum_actions_at(action.url, ret, function(a)
return a.type == "move" or a.type == "delete" return a.type == 'move' or a.type == 'delete'
end) end)
elseif action.type == "change" then elseif action.type == 'change' then
-- Finish operating on parents first -- Finish operating on parents first
-- e.g. NEW /a BEFORE CHANGE /a/b -- e.g. NEW /a BEFORE CHANGE /a/b
dest_trie:accum_first_parents_of(action.url, ret) dest_trie:accum_first_parents_of(action.url, ret)
@ -244,9 +244,9 @@ M.enforce_action_order = function(actions)
-- Finish copy from operations first -- Finish copy from operations first
-- e.g. COPY /a -> /b BEFORE CHANGE /a -- e.g. COPY /a -> /b BEFORE CHANGE /a
src_trie:accum_actions_at(action.url, ret, function(entry) src_trie:accum_actions_at(action.url, ret, function(entry)
return entry.type == "copy" return entry.type == 'copy'
end) end)
elseif action.type == "move" then elseif action.type == 'move' then
-- Finish operating on parents first -- Finish operating on parents first
-- e.g. NEW /a BEFORE MOVE /z -> /a/b -- e.g. NEW /a BEFORE MOVE /z -> /a/b
dest_trie:accum_first_parents_of(action.dest_url, ret) dest_trie:accum_first_parents_of(action.dest_url, ret)
@ -260,9 +260,9 @@ M.enforce_action_order = function(actions)
-- Process remove path before moving to new path -- Process remove path before moving to new path
-- e.g. MOVE /a -> /b BEFORE MOVE /c -> /a -- e.g. MOVE /a -> /b BEFORE MOVE /c -> /a
src_trie:accum_actions_at(action.dest_url, ret, function(a) src_trie:accum_actions_at(action.dest_url, ret, function(a)
return a.type == "move" or a.type == "delete" return a.type == 'move' or a.type == 'delete'
end) end)
elseif action.type == "copy" then elseif action.type == 'copy' then
-- Finish operating on parents first -- Finish operating on parents first
-- e.g. NEW /a BEFORE COPY /z -> /a/b -- e.g. NEW /a BEFORE COPY /z -> /a/b
dest_trie:accum_first_parents_of(action.dest_url, ret) dest_trie:accum_first_parents_of(action.dest_url, ret)
@ -272,7 +272,7 @@ M.enforce_action_order = function(actions)
-- Process remove path before copying to new path -- Process remove path before copying to new path
-- e.g. MOVE /a -> /b BEFORE COPY /c -> /a -- e.g. MOVE /a -> /b BEFORE COPY /c -> /a
src_trie:accum_actions_at(action.dest_url, ret, function(a) src_trie:accum_actions_at(action.dest_url, ret, function(a)
return a.type == "move" or a.type == "delete" return a.type == 'move' or a.type == 'delete'
end) end)
end end
return ret return ret
@ -312,24 +312,24 @@ M.enforce_action_order = function(actions)
if selected then if selected then
to_remove = selected to_remove = selected
else else
if loop_action and loop_action.type == "move" then if loop_action and loop_action.type == 'move' then
-- If this is moving a parent into itself, that's an error -- If this is moving a parent into itself, that's an error
if vim.startswith(loop_action.dest_url, loop_action.src_url) then if vim.startswith(loop_action.dest_url, loop_action.src_url) then
error("Detected cycle in desired paths") error('Detected cycle in desired paths')
end end
-- We've detected a move cycle (e.g. MOVE /a -> /b + MOVE /b -> /a) -- We've detected a move cycle (e.g. MOVE /a -> /b + MOVE /b -> /a)
-- Split one of the moves and retry -- Split one of the moves and retry
local intermediate_url = local intermediate_url =
string.format("%s__oil_tmp_%05d", loop_action.src_url, math.random(999999)) string.format('%s__oil_tmp_%05d', loop_action.src_url, math.random(999999))
local move_1 = { local move_1 = {
type = "move", type = 'move',
entry_type = loop_action.entry_type, entry_type = loop_action.entry_type,
src_url = loop_action.src_url, src_url = loop_action.src_url,
dest_url = intermediate_url, dest_url = intermediate_url,
} }
local move_2 = { local move_2 = {
type = "move", type = 'move',
entry_type = loop_action.entry_type, entry_type = loop_action.entry_type,
src_url = intermediate_url, src_url = intermediate_url,
dest_url = loop_action.dest_url, dest_url = loop_action.dest_url,
@ -340,16 +340,16 @@ M.enforce_action_order = function(actions)
dest_trie:insert_action(move_1.dest_url, move_1) dest_trie:insert_action(move_1.dest_url, move_1)
src_trie:insert_action(move_1.src_url, move_1) src_trie:insert_action(move_1.src_url, move_1)
else else
error("Detected cycle in desired paths") error('Detected cycle in desired paths')
end end
end end
if selected then if selected then
if selected.type == "move" or selected.type == "copy" then if selected.type == 'move' or selected.type == 'copy' then
if vim.startswith(selected.dest_url, selected.src_url .. "/") then if vim.startswith(selected.dest_url, selected.src_url .. '/') then
error( error(
string.format( string.format(
"Cannot move or copy parent into itself: %s -> %s", 'Cannot move or copy parent into itself: %s -> %s',
selected.src_url, selected.src_url,
selected.dest_url selected.dest_url
) )
@ -360,9 +360,9 @@ M.enforce_action_order = function(actions)
end end
if to_remove then if to_remove then
if to_remove.type == "delete" or to_remove.type == "change" then if to_remove.type == 'delete' or to_remove.type == 'change' then
src_trie:remove_action(to_remove.url, to_remove) src_trie:remove_action(to_remove.url, to_remove)
elseif to_remove.type == "create" then elseif to_remove.type == 'create' then
dest_trie:remove_action(to_remove.url, to_remove) dest_trie:remove_action(to_remove.url, to_remove)
else else
dest_trie:remove_action(to_remove.dest_url, to_remove) dest_trie:remove_action(to_remove.dest_url, to_remove)
@ -387,8 +387,8 @@ local progress
---@param cb fun(err: nil|string) ---@param cb fun(err: nil|string)
M.process_actions = function(actions, cb) M.process_actions = function(actions, cb)
vim.api.nvim_exec_autocmds( vim.api.nvim_exec_autocmds(
"User", 'User',
{ pattern = "OilActionsPre", modeline = false, data = { actions = actions } } { pattern = 'OilActionsPre', modeline = false, data = { actions = actions } }
) )
local did_complete = nil local did_complete = nil
@ -398,14 +398,14 @@ M.process_actions = function(actions, cb)
-- Convert some cross-adapter moves to a copy + delete -- Convert some cross-adapter moves to a copy + delete
for _, action in ipairs(actions) do for _, action in ipairs(actions) do
if action.type == "move" then if action.type == 'move' then
local _, cross_action = util.get_adapter_for_action(action) local _, cross_action = util.get_adapter_for_action(action)
-- Only do the conversion if the cross-adapter support is "copy" -- Only do the conversion if the cross-adapter support is "copy"
if cross_action == "copy" then if cross_action == 'copy' then
---@diagnostic disable-next-line: assign-type-mismatch ---@diagnostic disable-next-line: assign-type-mismatch
action.type = "copy" action.type = 'copy'
table.insert(actions, { table.insert(actions, {
type = "delete", type = 'delete',
url = action.src_url, url = action.src_url,
entry_type = action.entry_type, entry_type = action.entry_type,
}) })
@ -421,8 +421,8 @@ M.process_actions = function(actions, cb)
progress:close() progress:close()
progress = nil progress = nil
vim.api.nvim_exec_autocmds( vim.api.nvim_exec_autocmds(
"User", 'User',
{ pattern = "OilActionsPost", modeline = false, data = { err = err, actions = actions } } { pattern = 'OilActionsPost', modeline = false, data = { err = err, actions = actions } }
) )
cb(err) cb(err)
end end
@ -435,7 +435,7 @@ M.process_actions = function(actions, cb)
-- TODO some actions are actually cancelable. -- TODO some actions are actually cancelable.
-- We should stop them instead of stopping after the current action -- We should stop them instead of stopping after the current action
cancel = function() cancel = function()
finish("Canceled") finish('Canceled')
end, end,
}) })
end end
@ -472,7 +472,7 @@ M.process_actions = function(actions, cb)
next_action() next_action()
end end
end) end)
if action.type == "change" then if action.type == 'change' then
---@cast action oil.ChangeAction ---@cast action oil.ChangeAction
columns.perform_change_action(adapter, action, callback) columns.perform_change_action(adapter, action, callback)
else else
@ -502,7 +502,7 @@ M.try_write_changes = function(confirm, cb)
cb = function(_err) end cb = function(_err) end
end end
if mutation_in_progress then if mutation_in_progress then
cb("Cannot perform mutation when already in progress") cb('Cannot perform mutation when already in progress')
return return
end end
local current_buf = vim.api.nvim_get_current_buf() local current_buf = vim.api.nvim_get_current_buf()
@ -537,7 +537,7 @@ M.try_write_changes = function(confirm, cb)
mutation_in_progress = false mutation_in_progress = false
end end
local ns = vim.api.nvim_create_namespace("Oil") local ns = vim.api.nvim_create_namespace('Oil')
vim.diagnostic.reset(ns) vim.diagnostic.reset(ns)
if not vim.tbl_isempty(all_errors) then if not vim.tbl_isempty(all_errors) then
for bufnr, errors in pairs(all_errors) do for bufnr, errors in pairs(all_errors) do
@ -564,7 +564,7 @@ M.try_write_changes = function(confirm, cb)
end) end)
end end
unlock() unlock()
cb("Error parsing oil buffers") cb('Error parsing oil buffers')
return return
end end
@ -572,7 +572,7 @@ M.try_write_changes = function(confirm, cb)
confirmation.show(actions, confirm, function(proceed) confirmation.show(actions, confirm, function(proceed)
if not proceed then if not proceed then
unlock() unlock()
cb("Canceled") cb('Canceled')
return return
end end
@ -581,7 +581,7 @@ M.try_write_changes = function(confirm, cb)
vim.schedule_wrap(function(err) vim.schedule_wrap(function(err)
view.unlock_buffers() view.unlock_buffers()
if err then if err then
err = string.format("[oil] Error applying actions: %s", err) err = string.format('[oil] Error applying actions: %s', err)
view.rerender_all_oil_buffers(nil, function() view.rerender_all_oil_buffers(nil, function()
cb(err) cb(err)
end) end)
@ -591,13 +591,13 @@ M.try_write_changes = function(confirm, cb)
-- get the entry under the cursor and make sure the cursor stays on it -- get the entry under the cursor and make sure the cursor stays on it
view.set_last_cursor( view.set_last_cursor(
vim.api.nvim_buf_get_name(0), vim.api.nvim_buf_get_name(0),
vim.split(current_entry.parsed_name or current_entry.name, "/")[1] vim.split(current_entry.parsed_name or current_entry.name, '/')[1]
) )
end end
view.rerender_all_oil_buffers(nil, function(render_err) view.rerender_all_oil_buffers(nil, function(render_err)
vim.api.nvim_exec_autocmds( vim.api.nvim_exec_autocmds(
"User", 'User',
{ pattern = "OilMutationComplete", modeline = false } { pattern = 'OilMutationComplete', modeline = false }
) )
cb(render_err) cb(render_err)
end) end)

View file

@ -1,10 +1,10 @@
local cache = require("oil.cache") local cache = require('oil.cache')
local columns = require("oil.columns") local columns = require('oil.columns')
local config = require("oil.config") local config = require('oil.config')
local constants = require("oil.constants") local constants = require('oil.constants')
local fs = require("oil.fs") local fs = require('oil.fs')
local util = require("oil.util") local util = require('oil.util')
local view = require("oil.view") local view = require('oil.view')
local M = {} local M = {}
local FIELD_ID = constants.FIELD_ID local FIELD_ID = constants.FIELD_ID
@ -37,7 +37,7 @@ local FIELD_META = constants.FIELD_META
---@return string ---@return string
---@return boolean ---@return boolean
local function parsedir(name) local function parsedir(name)
local isdir = vim.endswith(name, "/") or (fs.is_windows and vim.endswith(name, "\\")) local isdir = vim.endswith(name, '/') or (fs.is_windows and vim.endswith(name, '\\'))
if isdir then if isdir then
name = name:sub(1, name:len() - 1) name = name:sub(1, name:len() - 1)
end end
@ -52,8 +52,8 @@ local function compare_link_target(meta, parsed_entry)
return false return false
end end
-- Make sure we trim off any trailing path slashes from both sources -- Make sure we trim off any trailing path slashes from both sources
local meta_name = meta.link:gsub("[/\\]$", "") local meta_name = meta.link:gsub('[/\\]$', '')
local parsed_name = parsed_entry.link_target:gsub("[/\\]$", "") local parsed_name = parsed_entry.link_target:gsub('[/\\]$', '')
return meta_name == parsed_name return meta_name == parsed_name
end end
@ -72,9 +72,9 @@ M.parse_line = function(adapter, line, column_defs)
local ret = {} local ret = {}
local ranges = {} local ranges = {}
local start = 1 local start = 1
local value, rem = line:match("^/(%d+) (.+)$") local value, rem = line:match('^/(%d+) (.+)$')
if not value then if not value then
return nil, "Malformed ID at start of line" return nil, 'Malformed ID at start of line'
end end
ranges.id = { start, value:len() + 1 } ranges.id = { start, value:len() + 1 }
start = ranges.id[2] + 1 start = ranges.id[2] + 1
@ -97,7 +97,7 @@ M.parse_line = function(adapter, line, column_defs)
local start_len = string.len(rem) local start_len = string.len(rem)
value, rem = columns.parse_col(adapter, assert(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
ret[name] = value ret[name] = value
range[2] = range[1] + start_len - string.len(rem) - 1 range[2] = range[1] + start_len - string.len(rem) - 1
@ -108,10 +108,10 @@ M.parse_line = function(adapter, line, column_defs)
if name then if name then
local isdir local isdir
name, isdir = parsedir(vim.trim(name)) name, isdir = parsedir(vim.trim(name))
if name ~= "" then if name ~= '' then
ret.name = name ret.name = name
end end
ret._type = isdir and "directory" or "file" ret._type = isdir and 'directory' or 'file'
end end
local entry = cache.get_entry_by_id(ret.id) local entry = cache.get_entry_by_id(ret.id)
ranges.name = { start, start + string.len(rem) - 1 } ranges.name = { start, start + string.len(rem) - 1 }
@ -122,20 +122,20 @@ M.parse_line = function(adapter, line, column_defs)
-- Parse the symlink syntax -- Parse the symlink syntax
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
local entry_type = entry[FIELD_TYPE] local entry_type = entry[FIELD_TYPE]
if entry_type == "link" and meta and meta.link then if entry_type == 'link' and meta and meta.link then
local name_pieces = vim.split(ret.name, " -> ", { plain = true }) local name_pieces = vim.split(ret.name, ' -> ', { plain = true })
if #name_pieces ~= 2 then if #name_pieces ~= 2 then
ret.name = "" ret.name = ''
return { data = ret, ranges = ranges } return { data = ret, ranges = ranges }
end end
ranges.name = { start, start + string.len(name_pieces[1]) - 1 } ranges.name = { start, start + string.len(name_pieces[1]) - 1 }
ret.name = parsedir(vim.trim(name_pieces[1])) ret.name = parsedir(vim.trim(name_pieces[1]))
ret.link_target = name_pieces[2] ret.link_target = name_pieces[2]
ret._type = "link" ret._type = 'link'
end end
-- Try to keep the same file type -- Try to keep the same file type
if entry_type ~= "directory" and entry_type ~= "file" and ret._type ~= "directory" then if entry_type ~= 'directory' and entry_type ~= 'file' and ret._type ~= 'directory' then
ret._type = entry[FIELD_TYPE] ret._type = entry[FIELD_TYPE]
end end
@ -187,7 +187,7 @@ M.parse = function(bufnr)
name = name:lower() name = name:lower()
end end
if seen_names[name] then if seen_names[name] then
table.insert(errors, { message = "Duplicate filename", lnum = i - 1, end_lnum = i, col = 0 }) table.insert(errors, { message = 'Duplicate filename', lnum = i - 1, end_lnum = i, col = 0 })
else else
seen_names[name] = true seen_names[name] = true
end end
@ -197,7 +197,7 @@ M.parse = function(bufnr)
-- hack to be compatible with Lua 5.1 -- hack to be compatible with Lua 5.1
-- use return instead of goto -- use return instead of goto
(function() (function()
if line:match("^/%d+") then if line:match('^/%d+') then
-- Parse the line for an existing entry -- Parse the line for an existing entry
local result, err = M.parse_line(adapter, line, column_defs) local result, err = M.parse_line(adapter, line, column_defs)
if not result or err then if not result or err then
@ -217,11 +217,11 @@ M.parse = function(bufnr)
local err_message local err_message
if not parsed_entry.name then if not parsed_entry.name then
err_message = "No filename found" err_message = 'No filename found'
elseif not entry then elseif not entry then
err_message = "Could not find existing entry (was the ID changed?)" err_message = 'Could not find existing entry (was the ID changed?)'
elseif parsed_entry.name:match("/") or parsed_entry.name:match(fs.sep) then elseif parsed_entry.name:match('/') or parsed_entry.name:match(fs.sep) then
err_message = "Filename cannot contain path separator" err_message = 'Filename cannot contain path separator'
end end
if err_message then if err_message then
table.insert(errors, { table.insert(errors, {
@ -237,16 +237,16 @@ M.parse = function(bufnr)
check_dupe(parsed_entry.name, i) check_dupe(parsed_entry.name, i)
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
if original_entries[parsed_entry.name] == parsed_entry.id then if original_entries[parsed_entry.name] == parsed_entry.id then
if entry[FIELD_TYPE] == "link" and not compare_link_target(meta, parsed_entry) then if entry[FIELD_TYPE] == 'link' and not compare_link_target(meta, parsed_entry) then
table.insert(diffs, { table.insert(diffs, {
type = "new", type = 'new',
name = parsed_entry.name, name = parsed_entry.name,
entry_type = "link", entry_type = 'link',
link = parsed_entry.link_target, link = parsed_entry.link_target,
}) })
elseif entry[FIELD_TYPE] ~= parsed_entry._type then elseif entry[FIELD_TYPE] ~= parsed_entry._type then
table.insert(diffs, { table.insert(diffs, {
type = "new", type = 'new',
name = parsed_entry.name, name = parsed_entry.name,
entry_type = parsed_entry._type, entry_type = parsed_entry._type,
}) })
@ -255,7 +255,7 @@ M.parse = function(bufnr)
end end
else else
table.insert(diffs, { table.insert(diffs, {
type = "new", type = 'new',
name = parsed_entry.name, name = parsed_entry.name,
entry_type = parsed_entry._type, entry_type = parsed_entry._type,
id = parsed_entry.id, id = parsed_entry.id,
@ -267,7 +267,7 @@ M.parse = function(bufnr)
local col_name = util.split_config(col_def) local col_name = util.split_config(col_def)
if columns.compare(adapter, col_name, entry, parsed_entry[col_name]) then if columns.compare(adapter, col_name, entry, parsed_entry[col_name]) then
table.insert(diffs, { table.insert(diffs, {
type = "change", type = 'change',
name = parsed_entry.name, name = parsed_entry.name,
entry_type = entry[FIELD_TYPE], entry_type = entry[FIELD_TYPE],
column = col_name, column = col_name,
@ -278,7 +278,7 @@ M.parse = function(bufnr)
else else
-- Parse a new entry -- Parse a new entry
local name, isdir = parsedir(vim.trim(line)) local name, isdir = parsedir(vim.trim(line))
if vim.startswith(name, "/") then if vim.startswith(name, '/') then
table.insert(errors, { table.insert(errors, {
message = "Paths cannot start with '/'", message = "Paths cannot start with '/'",
lnum = i - 1, lnum = i - 1,
@ -287,17 +287,17 @@ M.parse = function(bufnr)
}) })
return return
end end
if name ~= "" then if name ~= '' then
local link_pieces = vim.split(name, " -> ", { plain = true }) local link_pieces = vim.split(name, ' -> ', { plain = true })
local entry_type = isdir and "directory" or "file" local entry_type = isdir and 'directory' or 'file'
local link local link
if #link_pieces == 2 then if #link_pieces == 2 then
entry_type = "link" entry_type = 'link'
name, link = unpack(link_pieces) name, link = unpack(link_pieces)
end end
check_dupe(name, i) check_dupe(name, i)
table.insert(diffs, { table.insert(diffs, {
type = "new", type = 'new',
name = name, name = name,
entry_type = entry_type, entry_type = entry_type,
link = link, link = link,
@ -309,7 +309,7 @@ M.parse = function(bufnr)
for name, child_id in pairs(original_entries) do for name, child_id in pairs(original_entries) do
table.insert(diffs, { table.insert(diffs, {
type = "delete", type = 'delete',
name = name, name = name,
id = child_id, id = child_id,
}) })

View file

@ -1,17 +1,17 @@
local columns = require("oil.columns") local columns = require('oil.columns')
local config = require("oil.config") local config = require('oil.config')
local layout = require("oil.layout") local layout = require('oil.layout')
local loading = require("oil.loading") local loading = require('oil.loading')
local util = require("oil.util") local util = require('oil.util')
local Progress = {} local Progress = {}
local FPS = 20 local FPS = 20
function Progress.new() function Progress.new()
return setmetatable({ return setmetatable({
lines = { "", "" }, lines = { '', '' },
count = "", count = '',
spinner = "", spinner = '',
bufnr = nil, bufnr = nil,
winid = nil, winid = nil,
min_bufnr = nil, min_bufnr = nil,
@ -40,18 +40,18 @@ function Progress:show(opts)
return return
end end
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].bufhidden = "wipe" vim.bo[bufnr].bufhidden = 'wipe'
self.bufnr = bufnr self.bufnr = bufnr
self.cancel = opts.cancel or self.cancel self.cancel = opts.cancel or self.cancel
local loading_iter = loading.get_bar_iter() local loading_iter = loading.get_bar_iter()
local spinner = loading.get_iter("dots") local spinner = loading.get_iter('dots')
if not self.timer then if not self.timer then
self.timer = vim.loop.new_timer() self.timer = vim.loop.new_timer()
self.timer:start( self.timer:start(
0, 0,
math.floor(1000 / FPS), math.floor(1000 / FPS),
vim.schedule_wrap(function() vim.schedule_wrap(function()
self.lines[2] = string.format("%s %s", self.count, loading_iter()) self.lines[2] = string.format('%s %s', self.count, loading_iter())
self.spinner = spinner() self.spinner = spinner()
self:_render() self:_render()
end) end)
@ -59,22 +59,22 @@ function Progress:show(opts)
end end
local width, height = layout.calculate_dims(120, 10, config.progress) local width, height = layout.calculate_dims(120, 10, config.progress)
self.winid = vim.api.nvim_open_win(self.bufnr, true, { self.winid = vim.api.nvim_open_win(self.bufnr, true, {
relative = "editor", relative = 'editor',
width = width, width = width,
height = height, height = height,
row = math.floor((layout.get_editor_height() - height) / 2), row = math.floor((layout.get_editor_height() - height) / 2),
col = math.floor((layout.get_editor_width() - width) / 2), col = math.floor((layout.get_editor_width() - width) / 2),
zindex = 152, -- render on top of the floating window title zindex = 152, -- render on top of the floating window title
style = "minimal", style = 'minimal',
border = config.progress.border, border = config.progress.border,
}) })
vim.bo[self.bufnr].filetype = "oil_progress" vim.bo[self.bufnr].filetype = 'oil_progress'
for k, v in pairs(config.progress.win_options) do for k, v in pairs(config.progress.win_options) do
vim.api.nvim_set_option_value(k, v, { scope = "local", win = self.winid }) vim.api.nvim_set_option_value(k, v, { scope = 'local', win = self.winid })
end end
table.insert( table.insert(
self.autocmds, self.autocmds,
vim.api.nvim_create_autocmd("VimResized", { vim.api.nvim_create_autocmd('VimResized', {
callback = function() callback = function()
self:_reposition() self:_reposition()
end, end,
@ -82,7 +82,7 @@ function Progress:show(opts)
) )
table.insert( table.insert(
self.autocmds, self.autocmds,
vim.api.nvim_create_autocmd("WinLeave", { vim.api.nvim_create_autocmd('WinLeave', {
callback = function() callback = function()
self:minimize() self:minimize()
end, end,
@ -94,17 +94,17 @@ function Progress:show(opts)
vim.api.nvim_win_close(self.winid, true) vim.api.nvim_win_close(self.winid, true)
end end
end end
vim.keymap.set("n", "c", cancel, { buffer = self.bufnr, nowait = true }) vim.keymap.set('n', 'c', cancel, { buffer = self.bufnr, nowait = true })
vim.keymap.set("n", "C", cancel, { buffer = self.bufnr, nowait = true }) vim.keymap.set('n', 'C', cancel, { buffer = self.bufnr, nowait = true })
vim.keymap.set("n", "m", minimize, { buffer = self.bufnr, nowait = true }) vim.keymap.set('n', 'm', minimize, { buffer = self.bufnr, nowait = true })
vim.keymap.set("n", "M", minimize, { buffer = self.bufnr, nowait = true }) vim.keymap.set('n', 'M', minimize, { buffer = self.bufnr, nowait = true })
end end
function Progress:restore() function Progress:restore()
if self.closing then if self.closing then
return return
elseif not self:is_minimized() then elseif not self:is_minimized() then
error("Cannot restore progress window: not minimized") error('Cannot restore progress window: not minimized')
end end
self:_cleanup_minimized_win() self:_cleanup_minimized_win()
self:show() self:show()
@ -115,14 +115,14 @@ function Progress:_render()
util.render_text( util.render_text(
self.bufnr, self.bufnr,
self.lines, self.lines,
{ winid = self.winid, actions = { "[M]inimize", "[C]ancel" } } { winid = self.winid, actions = { '[M]inimize', '[C]ancel' } }
) )
end end
if self.min_bufnr and vim.api.nvim_buf_is_valid(self.min_bufnr) then if self.min_bufnr and vim.api.nvim_buf_is_valid(self.min_bufnr) then
util.render_text( util.render_text(
self.min_bufnr, self.min_bufnr,
{ string.format("%sOil: %s", self.spinner, self.count) }, { string.format('%sOil: %s', self.spinner, self.count) },
{ winid = self.min_winid, h_align = "left" } { winid = self.min_winid, h_align = 'left' }
) )
end end
end end
@ -136,7 +136,7 @@ function Progress:_reposition()
end end
local width, height = layout.calculate_dims(min_width, 10, config.progress) local width, height = layout.calculate_dims(min_width, 10, config.progress)
vim.api.nvim_win_set_config(self.winid, { vim.api.nvim_win_set_config(self.winid, {
relative = "editor", relative = 'editor',
width = width, width = width,
height = height, height = height,
row = math.floor((layout.get_editor_height() - height) / 2), row = math.floor((layout.get_editor_height() - height) / 2),
@ -174,22 +174,22 @@ function Progress:minimize()
end end
self:_cleanup_main_win() self:_cleanup_main_win()
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].bufhidden = "wipe" vim.bo[bufnr].bufhidden = 'wipe'
local winid = vim.api.nvim_open_win(bufnr, false, { local winid = vim.api.nvim_open_win(bufnr, false, {
relative = "editor", relative = 'editor',
width = 16, width = 16,
height = 1, height = 1,
anchor = "SE", anchor = 'SE',
row = layout.get_editor_height(), row = layout.get_editor_height(),
col = layout.get_editor_width(), col = layout.get_editor_width(),
zindex = 152, -- render on top of the floating window title zindex = 152, -- render on top of the floating window title
style = "minimal", style = 'minimal',
border = config.progress.minimized_border, border = config.progress.minimized_border,
}) })
self.min_bufnr = bufnr self.min_bufnr = bufnr
self.min_winid = winid self.min_winid = winid
self:_render() self:_render()
vim.notify_once("Restore progress window with :Oil --progress") vim.notify_once('Restore progress window with :Oil --progress')
end end
---@param action oil.Action ---@param action oil.Action
@ -198,14 +198,14 @@ end
function Progress:set_action(action, idx, total) function Progress:set_action(action, idx, total)
local adapter = util.get_adapter_for_action(action) local adapter = util.get_adapter_for_action(action)
local change_line local change_line
if action.type == "change" then if action.type == 'change' then
---@cast action oil.ChangeAction ---@cast action oil.ChangeAction
change_line = columns.render_change_action(adapter, action) change_line = columns.render_change_action(adapter, action)
else else
change_line = adapter.render_action(action) change_line = adapter.render_action(action)
end end
self.lines[1] = change_line self.lines[1] = change_line
self.count = string.format("%d/%d", idx, total) self.count = string.format('%d/%d', idx, total)
self:_reposition() self:_reposition()
self:_render() self:_render()
end end

View file

@ -1,4 +1,4 @@
local util = require("oil.util") local util = require('oil.util')
---@class (exact) oil.Trie ---@class (exact) oil.Trie
---@field new fun(): oil.Trie ---@field new fun(): oil.Trie
@ -20,7 +20,7 @@ end
function Trie:_url_to_path_pieces(url) function Trie:_url_to_path_pieces(url)
local scheme, path = util.parse_url(url) local scheme, path = util.parse_url(url)
assert(path) assert(path)
local pieces = vim.split(path, "/") local pieces = vim.split(path, '/')
table.insert(pieces, 1, scheme) table.insert(pieces, 1, scheme)
return pieces return pieces
end end
@ -75,7 +75,7 @@ function Trie:remove(path_pieces, value)
return return
end end
end end
error("Value not present in trie: " .. vim.inspect(value)) error('Value not present in trie: ' .. vim.inspect(value))
end end
---Add the first action that affects a parent path of the url ---Add the first action that affects a parent path of the url

View file

@ -3,26 +3,26 @@ local M = {}
---@param path string ---@param path string
---@return string ---@return string
M.parent = function(path) M.parent = function(path)
if path == "/" then if path == '/' then
return "/" return '/'
elseif path == "" then elseif path == '' then
return "" return ''
elseif vim.endswith(path, "/") then elseif vim.endswith(path, '/') then
return path:match("^(.*/)[^/]*/$") or "" return path:match('^(.*/)[^/]*/$') or ''
else else
return path:match("^(.*/)[^/]*$") or "" return path:match('^(.*/)[^/]*$') or ''
end end
end end
---@param path string ---@param path string
---@return nil|string ---@return nil|string
M.basename = function(path) M.basename = function(path)
if path == "/" or path == "" then if path == '/' or path == '' then
return return
elseif vim.endswith(path, "/") then elseif vim.endswith(path, '/') then
return path:match("^.*/([^/]*)/$") return path:match('^.*/([^/]*)/$')
else else
return path:match("^.*/([^/]*)$") return path:match('^.*/([^/]*)$')
end end
end end

View file

@ -23,11 +23,11 @@ end
---@return string ---@return string
function Ringbuf:as_str() function Ringbuf:as_str()
local postfix = "" local postfix = ''
for i = 1, self.tail, 1 do for i = 1, self.tail, 1 do
postfix = postfix .. self.buf[i] postfix = postfix .. self.buf[i]
end end
local prefix = "" local prefix = ''
for i = self.tail + 1, #self.buf, 1 do for i = self.tail + 1, #self.buf, 1 do
prefix = prefix .. self.buf[i] prefix = prefix .. self.buf[i]
end end

View file

@ -9,7 +9,7 @@ M.run = function(cmd, opts, callback)
local stderr = {} local stderr = {}
local jid = vim.fn.jobstart( local jid = vim.fn.jobstart(
cmd, cmd,
vim.tbl_deep_extend("keep", opts, { vim.tbl_deep_extend('keep', opts, {
stdout_buffered = true, stdout_buffered = true,
stderr_buffered = true, stderr_buffered = true,
on_stdout = function(j, output) on_stdout = function(j, output)
@ -22,19 +22,19 @@ M.run = function(cmd, opts, callback)
if code == 0 then if code == 0 then
callback(nil, stdout) callback(nil, stdout)
else else
local err = table.concat(stderr, "\n") local err = table.concat(stderr, '\n')
if err == "" then if err == '' then
err = "Unknown error" err = 'Unknown error'
end end
local cmd_str = type(cmd) == "string" and cmd or table.concat(cmd, " ") local cmd_str = type(cmd) == 'string' and cmd or table.concat(cmd, ' ')
callback(string.format("Error running command '%s'\n%s", cmd_str, err)) callback(string.format("Error running command '%s'\n%s", cmd_str, err))
end end
end), end),
}) })
) )
local exe local exe
if type(cmd) == "string" then if type(cmd) == 'string' then
exe = vim.split(cmd, "%s+")[1] exe = vim.split(cmd, '%s+')[1]
else else
exe = cmd[1] exe = cmd[1]
end end

View file

@ -1,5 +1,5 @@
local config = require("oil.config") local config = require('oil.config')
local constants = require("oil.constants") local constants = require('oil.constants')
local M = {} local M = {}
@ -14,7 +14,7 @@ local FIELD_META = constants.FIELD_META
---@return nil|string ---@return nil|string
---@return nil|string ---@return nil|string
M.parse_url = function(url) M.parse_url = function(url)
return url:match("^(.*://)(.*)$") return url:match('^(.*://)(.*)$')
end end
---Escapes a filename for use in :edit ---Escapes a filename for use in :edit
@ -26,51 +26,51 @@ M.escape_filename = function(filename)
end end
local _url_escape_to_char = { local _url_escape_to_char = {
["20"] = " ", ['20'] = ' ',
["22"] = "", ['22'] = '',
["23"] = "#", ['23'] = '#',
["24"] = "$", ['24'] = '$',
["25"] = "%", ['25'] = '%',
["26"] = "&", ['26'] = '&',
["27"] = "", ['27'] = '',
["2B"] = "+", ['2B'] = '+',
["2C"] = ",", ['2C'] = ',',
["2F"] = "/", ['2F'] = '/',
["3A"] = ":", ['3A'] = ':',
["3B"] = ";", ['3B'] = ';',
["3C"] = "<", ['3C'] = '<',
["3D"] = "=", ['3D'] = '=',
["3E"] = ">", ['3E'] = '>',
["3F"] = "?", ['3F'] = '?',
["40"] = "@", ['40'] = '@',
["5B"] = "[", ['5B'] = '[',
["5C"] = "\\", ['5C'] = '\\',
["5D"] = "]", ['5D'] = ']',
["5E"] = "^", ['5E'] = '^',
["60"] = "`", ['60'] = '`',
["7B"] = "{", ['7B'] = '{',
["7C"] = "|", ['7C'] = '|',
["7D"] = "}", ['7D'] = '}',
["7E"] = "~", ['7E'] = '~',
} }
local _char_to_url_escape = {} local _char_to_url_escape = {}
for k, v in pairs(_url_escape_to_char) do for k, v in pairs(_url_escape_to_char) do
_char_to_url_escape[v] = "%" .. k _char_to_url_escape[v] = '%' .. k
end end
-- TODO this uri escape handling is very incomplete -- 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(".", _char_to_url_escape)) return (string:gsub('.', _char_to_url_escape))
end end
---@param string string ---@param string string
---@return string ---@return string
M.url_unescape = function(string) M.url_unescape = function(string)
return ( return (
string:gsub("%%([0-9A-Fa-f][0-9A-Fa-f])", function(seq) string:gsub('%%([0-9A-Fa-f][0-9A-Fa-f])', function(seq)
return _url_escape_to_char[seq:upper()] or ("%" .. seq) return _url_escape_to_char[seq:upper()] or ('%' .. seq)
end) end)
) )
end end
@ -105,14 +105,14 @@ M.pad_align = function(text, width, align)
return text, 0 return text, 0
end end
if align == "right" then if align == 'right' then
return string.rep(" ", total_pad) .. text, total_pad return string.rep(' ', total_pad) .. text, total_pad
elseif align == "center" then elseif align == 'center' then
local left_pad = math.floor(total_pad / 2) local left_pad = math.floor(total_pad / 2)
local right_pad = total_pad - left_pad local right_pad = total_pad - left_pad
return string.rep(" ", left_pad) .. text .. string.rep(" ", right_pad), left_pad return string.rep(' ', left_pad) .. text .. string.rep(' ', right_pad), left_pad
else else
return text .. string.rep(" ", total_pad), 0 return text .. string.rep(' ', total_pad), 0
end end
end end
@ -150,7 +150,7 @@ end
---@param dest_buf_name string ---@param dest_buf_name string
---@return boolean True if the buffer was replaced instead of renamed ---@return boolean True if the buffer was replaced instead of renamed
M.rename_buffer = function(src_bufnr, dest_buf_name) M.rename_buffer = function(src_bufnr, dest_buf_name)
if type(src_bufnr) == "string" then if type(src_bufnr) == 'string' then
src_bufnr = vim.fn.bufadd(src_bufnr) src_bufnr = vim.fn.bufadd(src_bufnr)
if not vim.api.nvim_buf_is_loaded(src_bufnr) then if not vim.api.nvim_buf_is_loaded(src_bufnr) then
vim.api.nvim_buf_delete(src_bufnr, {}) vim.api.nvim_buf_delete(src_bufnr, {})
@ -164,7 +164,7 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
-- think that the new buffer conflicts with the file next time it tries to save. -- think that the new buffer conflicts with the file next time it tries to save.
if not vim.loop.fs_stat(dest_buf_name) then if not vim.loop.fs_stat(dest_buf_name) then
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch
local altbuf = vim.fn.bufnr("#") local altbuf = vim.fn.bufnr('#')
-- 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
@ -173,7 +173,7 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
-- where Neovim doesn't allow buffer modifications. -- where Neovim doesn't allow buffer modifications.
pcall(vim.api.nvim_buf_delete, vim.fn.bufadd(bufname), {}) 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
return false return false
@ -229,6 +229,7 @@ end
M.cb_collect = function(count, cb) M.cb_collect = function(count, cb)
return function(err) return function(err)
if err then if err then
-- selene: allow(mismatched_arg_count)
cb(err) cb(err)
cb = function() end cb = function() end
else else
@ -243,9 +244,9 @@ end
---@param url string ---@param url string
---@return string[] ---@return string[]
local function get_possible_buffer_names_from_url(url) local function get_possible_buffer_names_from_url(url)
local fs = require("oil.fs") local fs = require('oil.fs')
local scheme, path = M.parse_url(url) local scheme, path = M.parse_url(url)
if config.adapters[scheme] == "files" then if config.adapters[scheme] == 'files' then
assert(path) assert(path)
return { fs.posix_to_os_path(path) } return { fs.posix_to_os_path(path) }
end end
@ -258,7 +259,7 @@ end
M.update_moved_buffers = function(entry_type, src_url, dest_url) M.update_moved_buffers = function(entry_type, src_url, dest_url)
local src_buf_names = get_possible_buffer_names_from_url(src_url) local src_buf_names = get_possible_buffer_names_from_url(src_url)
local dest_buf_name = get_possible_buffer_names_from_url(dest_url)[1] local dest_buf_name = get_possible_buffer_names_from_url(dest_url)[1]
if entry_type ~= "directory" then if entry_type ~= 'directory' then
for _, src_buf_name in ipairs(src_buf_names) do for _, src_buf_name in ipairs(src_buf_names) do
M.rename_buffer(src_buf_name, dest_buf_name) M.rename_buffer(src_buf_name, dest_buf_name)
end end
@ -272,13 +273,13 @@ M.update_moved_buffers = function(entry_type, src_url, dest_url)
if vim.startswith(bufname, src_url) then if vim.startswith(bufname, src_url) then
-- Handle oil directory buffers -- Handle oil directory buffers
vim.api.nvim_buf_set_name(bufnr, dest_url .. bufname:sub(src_url:len() + 1)) vim.api.nvim_buf_set_name(bufnr, dest_url .. bufname:sub(src_url:len() + 1))
elseif bufname ~= "" and vim.bo[bufnr].buftype == "" then elseif bufname ~= '' and vim.bo[bufnr].buftype == '' then
-- Handle regular buffers -- Handle regular buffers
local scheme = M.parse_url(bufname) local scheme = M.parse_url(bufname)
-- If the buffer is a local file, make sure we're using the absolute path -- If the buffer is a local file, make sure we're using the absolute path
if not scheme then if not scheme then
bufname = vim.fn.fnamemodify(bufname, ":p") bufname = vim.fn.fnamemodify(bufname, ':p')
end end
for _, src_buf_name in ipairs(src_buf_names) do for _, src_buf_name in ipairs(src_buf_names) do
@ -296,13 +297,13 @@ end
---@return string ---@return string
---@return table|nil ---@return table|nil
M.split_config = function(name_or_config) M.split_config = function(name_or_config)
if type(name_or_config) == "string" then if type(name_or_config) == 'string' then
return name_or_config, nil return name_or_config, nil
else else
if not name_or_config[1] and name_or_config["1"] then if not name_or_config[1] and name_or_config['1'] then
-- This was likely loaded from json, so the first element got coerced to a string key -- This was likely loaded from json, so the first element got coerced to a string key
name_or_config[1] = name_or_config["1"] name_or_config[1] = name_or_config['1']
name_or_config["1"] = nil name_or_config['1'] = nil
end end
return name_or_config[1], name_or_config return name_or_config[1], name_or_config
end end
@ -324,7 +325,7 @@ M.render_table = function(lines, col_width, col_align)
local pieces = {} local pieces = {}
for i, chunk in ipairs(cols) do for i, chunk in ipairs(cols) do
local text, hl local text, hl
if type(chunk) == "table" then if type(chunk) == 'table' then
text = chunk[1] text = chunk[1]
hl = chunk[2] hl = chunk[2]
else else
@ -333,11 +334,11 @@ M.render_table = function(lines, col_width, col_align)
local unpadded_len = text:len() local unpadded_len = text:len()
local padding local padding
text, padding = M.pad_align(text, col_width[i], col_align[i] or "left") text, padding = M.pad_align(text, col_width[i], col_align[i] or 'left')
table.insert(pieces, text) table.insert(pieces, text)
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 }[]
-- Notice that col_start and col_end are relative position inside -- Notice that col_start and col_end are relative position inside
-- that col, so we need to add the offset to them -- that col, so we need to add the offset to them
@ -355,7 +356,7 @@ M.render_table = function(lines, col_width, col_align)
end end
col = col + text:len() + 1 col = col + text:len() + 1
end end
table.insert(str_lines, table.concat(pieces, " ")) table.insert(str_lines, table.concat(pieces, ' '))
end end
return str_lines, highlights return str_lines, highlights
end end
@ -363,7 +364,7 @@ end
---@param bufnr integer ---@param bufnr integer
---@param highlights any[][] List of highlights {group, lnum, col_start, col_end} ---@param highlights any[][] List of highlights {group, lnum, col_start, col_end}
M.set_highlights = function(bufnr, highlights) 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
local group, line, col_start, col_end = unpack(hl) local group, line, col_start, col_end = unpack(hl)
@ -379,12 +380,12 @@ end
---@param os_slash? boolean use os filesystem slash instead of posix slash ---@param os_slash? boolean use os filesystem slash instead of posix slash
---@return string ---@return string
M.addslash = function(path, os_slash) M.addslash = function(path, os_slash)
local slash = "/" local slash = '/'
if os_slash and require("oil.fs").is_windows then if os_slash and require('oil.fs').is_windows then
slash = "\\" slash = '\\'
end end
local endslash = path:match(slash .. "$") local endslash = path:match(slash .. '$')
if not endslash then if not endslash then
return path .. slash return path .. slash
else else
@ -395,7 +396,7 @@ end
---@param winid nil|integer ---@param winid nil|integer
---@return boolean ---@return boolean
M.is_floating_win = function(winid) M.is_floating_win = function(winid)
return vim.api.nvim_win_get_config(winid or 0).relative ~= "" return vim.api.nvim_win_get_config(winid or 0).relative ~= ''
end end
---Recalculate the window title for the current buffer ---Recalculate the window title for the current buffer
@ -410,10 +411,10 @@ M.get_title = function(winid)
local title = vim.api.nvim_buf_get_name(src_buf) local title = vim.api.nvim_buf_get_name(src_buf)
local scheme, path = M.parse_url(title) local scheme, path = M.parse_url(title)
if config.adapters[scheme] == "files" then if config.adapters[scheme] == 'files' then
assert(path) assert(path)
local fs = require("oil.fs") local fs = require('oil.fs')
title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~") title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ':~')
end end
return title return title
end end
@ -421,7 +422,7 @@ end
local winid_map = {} local winid_map = {}
M.add_title_to_win = function(winid, opts) M.add_title_to_win = function(winid, opts)
opts = opts or {} opts = opts or {}
opts.align = opts.align or "left" opts.align = opts.align or 'left'
if not vim.api.nvim_win_is_valid(winid) then if not vim.api.nvim_win_is_valid(winid) then
return return
end end
@ -438,18 +439,18 @@ M.add_title_to_win = function(winid, opts)
else else
bufnr = vim.api.nvim_create_buf(false, true) bufnr = vim.api.nvim_create_buf(false, true)
local col = 1 local col = 1
if opts.align == "center" then if opts.align == 'center' then
col = math.floor((vim.api.nvim_win_get_width(winid) - width) / 2) col = math.floor((vim.api.nvim_win_get_width(winid) - width) / 2)
elseif opts.align == "right" then elseif opts.align == 'right' then
col = vim.api.nvim_win_get_width(winid) - 1 - width col = vim.api.nvim_win_get_width(winid) - 1 - width
elseif opts.align ~= "left" then elseif opts.align ~= 'left' then
vim.notify( vim.notify(
string.format("Unknown oil window title alignment: '%s'", opts.align), string.format("Unknown oil window title alignment: '%s'", opts.align),
vim.log.levels.ERROR vim.log.levels.ERROR
) )
end end
title_winid = vim.api.nvim_open_win(bufnr, false, { title_winid = vim.api.nvim_open_win(bufnr, false, {
relative = "win", relative = 'win',
win = winid, win = winid,
width = width, width = width,
height = 1, height = 1,
@ -457,20 +458,20 @@ M.add_title_to_win = function(winid, opts)
col = col, col = col,
focusable = false, focusable = false,
zindex = 151, zindex = 151,
style = "minimal", style = 'minimal',
noautocmd = true, noautocmd = true,
}) })
winid_map[winid] = title_winid winid_map[winid] = title_winid
vim.api.nvim_set_option_value( vim.api.nvim_set_option_value(
"winblend", 'winblend',
vim.wo[winid].winblend, vim.wo[winid].winblend,
{ scope = "local", win = title_winid } { scope = 'local', win = title_winid }
) )
vim.bo[bufnr].bufhidden = "wipe" vim.bo[bufnr].bufhidden = 'wipe'
local update_autocmd = vim.api.nvim_create_autocmd("BufWinEnter", { local update_autocmd = vim.api.nvim_create_autocmd('BufWinEnter', {
desc = "Update oil floating window title when buffer changes", desc = 'Update oil floating window title when buffer changes',
pattern = "*", pattern = '*',
callback = function(params) callback = function(params)
local winbuf = params.buf local winbuf = params.buf
if vim.api.nvim_win_get_buf(winid) ~= winbuf then if vim.api.nvim_win_get_buf(winid) ~= winbuf then
@ -479,17 +480,17 @@ M.add_title_to_win = function(winid, opts)
local new_title = M.get_title(winid) local new_title = M.get_title(winid)
local new_width = local new_width =
math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(new_title)) math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(new_title))
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { " " .. new_title .. " " }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { ' ' .. new_title .. ' ' })
vim.bo[bufnr].modified = false vim.bo[bufnr].modified = false
vim.api.nvim_win_set_width(title_winid, new_width) vim.api.nvim_win_set_width(title_winid, new_width)
local new_col = 1 local new_col = 1
if opts.align == "center" then if opts.align == 'center' then
new_col = math.floor((vim.api.nvim_win_get_width(winid) - new_width) / 2) new_col = math.floor((vim.api.nvim_win_get_width(winid) - new_width) / 2)
elseif opts.align == "right" then elseif opts.align == 'right' then
new_col = vim.api.nvim_win_get_width(winid) - 1 - new_width new_col = vim.api.nvim_win_get_width(winid) - 1 - new_width
end end
vim.api.nvim_win_set_config(title_winid, { vim.api.nvim_win_set_config(title_winid, {
relative = "win", relative = 'win',
win = winid, win = winid,
row = -1, row = -1,
col = new_col, col = new_col,
@ -498,8 +499,8 @@ M.add_title_to_win = function(winid, opts)
}) })
end, end,
}) })
vim.api.nvim_create_autocmd("WinClosed", { vim.api.nvim_create_autocmd('WinClosed', {
desc = "Close oil floating window title when floating window closes", desc = 'Close oil floating window title when floating window closes',
pattern = tostring(winid), pattern = tostring(winid),
callback = function() callback = function()
if title_winid and vim.api.nvim_win_is_valid(title_winid) then if title_winid and vim.api.nvim_win_is_valid(title_winid) then
@ -512,12 +513,12 @@ M.add_title_to_win = function(winid, opts)
nested = true, nested = true,
}) })
end end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { " " .. title .. " " }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { ' ' .. title .. ' ' })
vim.bo[bufnr].modified = false vim.bo[bufnr].modified = false
vim.api.nvim_set_option_value( vim.api.nvim_set_option_value(
"winhighlight", 'winhighlight',
"Normal:FloatTitle,NormalFloat:FloatTitle", 'Normal:FloatTitle,NormalFloat:FloatTitle',
{ scope = "local", win = title_winid } { scope = 'local', win = title_winid }
) )
end end
@ -542,7 +543,7 @@ M.get_adapter_for_action = function(action)
else else
error( error(
string.format( string.format(
"Cannot copy files from %s -> %s; no cross-adapter transfer method found", 'Cannot copy files from %s -> %s; no cross-adapter transfer method found',
action.src_url, action.src_url,
action.dest_url action.dest_url
) )
@ -559,12 +560,12 @@ end
---@return string ---@return string
---@return integer ---@return integer
M.h_align = function(str, align, width) M.h_align = function(str, align, width)
if align == "center" then if align == 'center' then
local padding = math.floor((width - vim.api.nvim_strwidth(str)) / 2) local padding = math.floor((width - vim.api.nvim_strwidth(str)) / 2)
return string.rep(" ", padding) .. str, padding return string.rep(' ', padding) .. str, padding
elseif align == "right" then elseif align == 'right' then
local padding = width - vim.api.nvim_strwidth(str) local padding = width - vim.api.nvim_strwidth(str)
return string.rep(" ", padding) .. str, padding return string.rep(' ', padding) .. str, padding
else else
return str, 0 return str, 0
end end
@ -578,15 +579,15 @@ end
--- actions nil|string[] --- actions nil|string[]
--- winid nil|integer --- winid nil|integer
M.render_text = function(bufnr, text, opts) M.render_text = function(bufnr, text, opts)
opts = vim.tbl_deep_extend("keep", opts or {}, { opts = vim.tbl_deep_extend('keep', opts or {}, {
h_align = "center", h_align = 'center',
v_align = "center", v_align = 'center',
}) })
---@cast opts -nil ---@cast opts -nil
if not vim.api.nvim_buf_is_valid(bufnr) then if not vim.api.nvim_buf_is_valid(bufnr) then
return return
end end
if type(text) == "string" then if type(text) == 'string' then
text = { text } text = { text }
end end
local height = 40 local height = 40
@ -608,17 +609,17 @@ M.render_text = function(bufnr, text, opts)
local lines = {} local lines = {}
-- Add vertical spacing for vertical alignment -- Add vertical spacing for vertical alignment
if opts.v_align == "center" then if opts.v_align == 'center' then
for _ = 1, (height / 2) - (#text / 2) do for _ = 1, (height / 2) - (#text / 2) do
table.insert(lines, "") table.insert(lines, '')
end end
elseif opts.v_align == "bottom" then elseif opts.v_align == 'bottom' then
local num_lines = height local num_lines = height
if opts.actions then if opts.actions then
num_lines = num_lines - 2 num_lines = num_lines - 2
end end
while #lines + #text < num_lines do while #lines + #text < num_lines do
table.insert(lines, "") table.insert(lines, '')
end end
end end
@ -632,12 +633,12 @@ M.render_text = function(bufnr, text, opts)
local highlights = {} local highlights = {}
if opts.actions then if opts.actions then
while #lines < height - 1 do while #lines < height - 1 do
table.insert(lines, "") table.insert(lines, '')
end end
local last_line, padding = M.h_align(table.concat(opts.actions, " "), "center", width) local last_line, padding = M.h_align(table.concat(opts.actions, ' '), 'center', width)
local col = padding local col = padding
for _, action in ipairs(opts.actions) do for _, action in ipairs(opts.actions) do
table.insert(highlights, { "Special", #lines, col, col + 3 }) table.insert(highlights, { 'Special', #lines, col, col + 3 })
col = padding + action:len() + 4 col = padding + action:len() + 4
end end
table.insert(lines, last_line) table.insert(lines, last_line)
@ -656,10 +657,10 @@ end
M.run_in_fullscreen_win = function(bufnr, callback) M.run_in_fullscreen_win = function(bufnr, callback)
if not bufnr then if not bufnr then
bufnr = vim.api.nvim_create_buf(false, true) bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].bufhidden = "wipe" vim.bo[bufnr].bufhidden = 'wipe'
end end
local winid = vim.api.nvim_open_win(bufnr, false, { local winid = vim.api.nvim_open_win(bufnr, false, {
relative = "editor", relative = 'editor',
width = vim.o.columns, width = vim.o.columns,
height = vim.o.lines, height = vim.o.lines,
row = 0, row = 0,
@ -667,7 +668,7 @@ M.run_in_fullscreen_win = function(bufnr, callback)
noautocmd = true, noautocmd = true,
}) })
local winnr = vim.api.nvim_win_get_number(winid) local winnr = vim.api.nvim_win_get_number(winid)
vim.cmd.wincmd({ count = winnr, args = { "w" }, mods = { noautocmd = true } }) vim.cmd.wincmd({ count = winnr, args = { 'w' }, mods = { noautocmd = true } })
callback() callback()
vim.cmd.close({ count = winnr, mods = { noautocmd = true, emsg_silent = true } }) vim.cmd.close({ count = winnr, mods = { noautocmd = true, emsg_silent = true } })
end end
@ -676,9 +677,9 @@ end
---@return boolean ---@return boolean
M.is_oil_bufnr = function(bufnr) M.is_oil_bufnr = function(bufnr)
local filetype = vim.bo[bufnr].filetype local filetype = vim.bo[bufnr].filetype
if filetype == "oil" then if filetype == 'oil' then
return true return true
elseif filetype ~= "" then elseif filetype ~= '' then
-- If the filetype is set and is NOT "oil", then it's not an oil buffer -- If the filetype is set and is NOT "oil", then it's not an oil buffer
return false return false
end end
@ -693,10 +694,10 @@ M.hack_around_termopen_autocmd = function(prev_mode)
vim.defer_fn(function() vim.defer_fn(function()
local new_mode = vim.api.nvim_get_mode().mode local new_mode = vim.api.nvim_get_mode().mode
if new_mode ~= prev_mode then if new_mode ~= prev_mode then
if string.find(new_mode, "i") == 1 then if string.find(new_mode, 'i') == 1 then
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<ESC>", true, true, true), "n", false) vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<ESC>', true, true, true), 'n', false)
if string.find(prev_mode, "v") == 1 or string.find(prev_mode, "V") == 1 then if string.find(prev_mode, 'v') == 1 or string.find(prev_mode, 'V') == 1 then
vim.cmd.normal({ bang = true, args = { "gv" } }) vim.cmd.normal({ bang = true, args = { 'gv' } })
end end
end end
end end
@ -712,7 +713,7 @@ M.get_preview_win = function(opts)
if if
vim.api.nvim_win_is_valid(winid) vim.api.nvim_win_is_valid(winid)
and vim.wo[winid].previewwindow and vim.wo[winid].previewwindow
and (opts.include_not_owned or vim.w[winid]["oil_preview"]) and (opts.include_not_owned or vim.w[winid]['oil_preview'])
then then
return winid return winid
end end
@ -721,13 +722,13 @@ end
---@return fun() restore Function that restores the cursor ---@return fun() restore Function that restores the cursor
M.hide_cursor = function() M.hide_cursor = function()
vim.api.nvim_set_hl(0, "OilPreviewCursor", { nocombine = true, blend = 100 }) vim.api.nvim_set_hl(0, 'OilPreviewCursor', { nocombine = true, blend = 100 })
local original_guicursor = vim.go.guicursor local original_guicursor = vim.go.guicursor
vim.go.guicursor = "a:OilPreviewCursor/OilPreviewCursor" vim.go.guicursor = 'a:OilPreviewCursor/OilPreviewCursor'
return function() return function()
-- HACK: see https://github.com/neovim/neovim/issues/21018 -- HACK: see https://github.com/neovim/neovim/issues/21018
vim.go.guicursor = "a:" vim.go.guicursor = 'a:'
vim.cmd.redrawstatus() vim.cmd.redrawstatus()
vim.go.guicursor = original_guicursor vim.go.guicursor = original_guicursor
end end
@ -762,7 +763,7 @@ end
---@param opts {columns?: string[], no_cache?: boolean} ---@param opts {columns?: string[], no_cache?: boolean}
---@param callback fun(err: nil|string, entries: nil|oil.InternalEntry[]) ---@param callback fun(err: nil|string, entries: nil|oil.InternalEntry[])
M.adapter_list_all = function(adapter, url, opts, callback) M.adapter_list_all = function(adapter, url, opts, callback)
local cache = require("oil.cache") local cache = require('oil.cache')
if not opts.no_cache then if not opts.no_cache then
local entries = cache.list_url(url) local entries = cache.list_url(url)
if not vim.tbl_isempty(entries) then if not vim.tbl_isempty(entries) then
@ -790,23 +791,23 @@ end
---based on the provided options. ---based on the provided options.
---@param opts {target?: "qflist"|"loclist", action?: "r"|"a", only_matching_search?: boolean} ---@param opts {target?: "qflist"|"loclist", action?: "r"|"a", only_matching_search?: boolean}
M.send_to_quickfix = function(opts) M.send_to_quickfix = function(opts)
if type(opts) ~= "table" then if type(opts) ~= 'table' then
opts = {} opts = {}
end end
local oil = require("oil") local oil = require('oil')
local dir = oil.get_current_dir() local dir = oil.get_current_dir()
if type(dir) ~= "string" then if type(dir) ~= 'string' then
return return
end end
local range = M.get_visual_range() local range = M.get_visual_range()
if not range then if not range then
range = { start_lnum = 1, end_lnum = vim.fn.line("$") } range = { start_lnum = 1, end_lnum = vim.fn.line('$') }
end end
local match_all = not opts.only_matching_search local match_all = not opts.only_matching_search
local qf_entries = {} local qf_entries = {}
for i = range.start_lnum, range.end_lnum do for i = range.start_lnum, range.end_lnum do
local entry = oil.get_entry_on_line(0, i) local entry = oil.get_entry_on_line(0, i)
if entry and entry.type == "file" and (match_all or M.is_matching(entry)) then if entry and entry.type == 'file' and (match_all or M.is_matching(entry)) then
local qf_entry = { local qf_entry = {
filename = dir .. entry.name, filename = dir .. entry.name,
lnum = 1, lnum = 1,
@ -817,26 +818,26 @@ M.send_to_quickfix = function(opts)
end end
end end
if #qf_entries == 0 then if #qf_entries == 0 then
vim.notify("[oil] No entries found to send to quickfix", vim.log.levels.WARN) vim.notify('[oil] No entries found to send to quickfix', vim.log.levels.WARN)
return return
end end
vim.api.nvim_exec_autocmds("QuickFixCmdPre", {}) vim.api.nvim_exec_autocmds('QuickFixCmdPre', {})
local qf_title = "oil files" local qf_title = 'oil files'
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() 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() vim.cmd.copen()
end end
vim.api.nvim_exec_autocmds("QuickFixCmdPost", {}) vim.api.nvim_exec_autocmds('QuickFixCmdPost', {})
end end
---@return boolean ---@return boolean
M.is_visual_mode = function() M.is_visual_mode = function()
local mode = vim.api.nvim_get_mode().mode local mode = vim.api.nvim_get_mode().mode
return mode:match("^[vV]") ~= nil return mode:match('^[vV]') ~= nil
end end
---Get the current visual selection range. If not in visual mode, return nil. ---Get the current visual selection range. If not in visual mode, return nil.
@ -847,7 +848,7 @@ M.get_visual_range = function()
end end
-- This is the best way to get the visual selection at the moment -- This is the best way to get the visual selection at the moment
-- https://github.com/neovim/neovim/pull/13896 -- https://github.com/neovim/neovim/pull/13896
local _, start_lnum, _, _ = unpack(vim.fn.getpos("v")) local _, start_lnum, _, _ = unpack(vim.fn.getpos('v'))
local _, end_lnum, _, _, _ = unpack(vim.fn.getcurpos()) local _, end_lnum, _, _, _ = unpack(vim.fn.getcurpos())
if start_lnum > end_lnum then if start_lnum > end_lnum then
start_lnum, end_lnum = end_lnum, start_lnum start_lnum, end_lnum = end_lnum, start_lnum
@ -863,7 +864,7 @@ M.is_matching = function(entry)
if search_highlighting_is_off then if search_highlighting_is_off then
return true return true
end end
local pattern = vim.fn.getreg("/") local pattern = vim.fn.getreg('/')
local position_of_match = vim.fn.match(entry.name, pattern) local position_of_match = vim.fn.match(entry.name, pattern)
return position_of_match ~= -1 return position_of_match ~= -1
end end
@ -877,8 +878,8 @@ M.run_after_load = function(bufnr, callback)
if vim.b[bufnr].oil_ready then if vim.b[bufnr].oil_ready then
callback() callback()
else else
vim.api.nvim_create_autocmd("User", { vim.api.nvim_create_autocmd('User', {
pattern = "OilEnter", pattern = 'OilEnter',
callback = function(args) callback = function(args)
if args.data.buf == bufnr then if args.data.buf == bufnr then
vim.api.nvim_buf_call(bufnr, callback) vim.api.nvim_buf_call(bufnr, callback)
@ -892,12 +893,12 @@ end
---@param entry oil.Entry ---@param entry oil.Entry
---@return boolean ---@return boolean
M.is_directory = function(entry) M.is_directory = function(entry)
local is_directory = entry.type == "directory" local is_directory = entry.type == 'directory'
or ( or (
entry.type == "link" entry.type == 'link'
and entry.meta and entry.meta
and entry.meta.link_stat and entry.meta.link_stat
and entry.meta.link_stat.type == "directory" and entry.meta.link_stat.type == 'directory'
) )
return is_directory == true return is_directory == true
end end
@ -907,7 +908,7 @@ end
---@param entry oil.Entry ---@param entry oil.Entry
---@param callback fun(normalized_url: string) ---@param callback fun(normalized_url: string)
M.get_edit_path = function(bufnr, entry, callback) M.get_edit_path = function(bufnr, entry, callback)
local pathutil = require("oil.pathutil") local pathutil = require('oil.pathutil')
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)
@ -916,10 +917,10 @@ M.get_edit_path = function(bufnr, entry, callback)
local url = scheme .. dir .. entry.name local url = scheme .. dir .. entry.name
if M.is_directory(entry) then if M.is_directory(entry) then
url = url .. "/" url = url .. '/'
end end
if entry.name == ".." then if entry.name == '..' then
callback(scheme .. pathutil.parent(dir)) callback(scheme .. pathutil.parent(dir))
elseif adapter.get_entry_path then elseif adapter.get_entry_path then
adapter.get_entry_path(url, entry, callback) adapter.get_entry_path(url, entry, callback)
@ -932,33 +933,34 @@ end
---@return (oil.IconProvider)? ---@return (oil.IconProvider)?
M.get_icon_provider = function() M.get_icon_provider = function()
-- prefer mini.icons -- prefer mini.icons
local _, mini_icons = pcall(require, "mini.icons") local _, mini_icons = pcall(require, 'mini.icons')
-- selene: allow(global_usage)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
if _G.MiniIcons then -- `_G.MiniIcons` is a better check to see if the module is setup if _G.MiniIcons then
return function(type, name, conf, ft) return function(type, name, conf, ft)
if ft then if ft then
return mini_icons.get("filetype", ft) return mini_icons.get('filetype', ft)
end end
return mini_icons.get(type == "directory" and "directory" or "file", name) return mini_icons.get(type == 'directory' and 'directory' or 'file', name)
end end
end end
-- fallback to `nvim-web-devicons` -- fallback to `nvim-web-devicons`
local has_devicons, devicons = pcall(require, "nvim-web-devicons") local has_devicons, devicons = pcall(require, 'nvim-web-devicons')
if has_devicons then if has_devicons then
return function(type, name, conf, ft) return function(type, name, conf, ft)
if type == "directory" then if type == 'directory' then
return conf and conf.directory or "", "OilDirIcon" return conf and conf.directory or '', 'OilDirIcon'
else else
if ft then if ft then
local ft_icon, ft_hl = devicons.get_icon_by_filetype(ft) local ft_icon, ft_hl = devicons.get_icon_by_filetype(ft)
if ft_icon and ft_icon ~= "" then if ft_icon and ft_icon ~= '' then
return ft_icon, ft_hl return ft_icon, ft_hl
end end
end end
local icon, hl = devicons.get_icon(name) local icon, hl = devicons.get_icon(name)
hl = hl or "OilFileIcon" hl = hl or 'OilFileIcon'
icon = icon or (conf and conf.default_file or "") icon = icon or (conf and conf.default_file or '')
return icon, hl return icon, hl
end end
end end
@ -975,24 +977,25 @@ M.read_file_to_scratch_buffer = function(path, preview_method)
return return
end end
vim.bo[bufnr].bufhidden = "wipe" vim.bo[bufnr].bufhidden = 'wipe'
vim.bo[bufnr].buftype = "nofile" vim.bo[bufnr].buftype = 'nofile'
local has_lines, read_res local has_lines, read_res
if preview_method == "fast_scratch" then if preview_method == 'fast_scratch' then
has_lines, read_res = pcall(vim.fn.readfile, path, "", vim.o.lines) has_lines, read_res = pcall(vim.fn.readfile, path, '', vim.o.lines)
else else
has_lines, read_res = pcall(vim.fn.readfile, path) has_lines, read_res = pcall(vim.fn.readfile, path)
end 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)
if not ok then if not ok then
return return
end end
local ft = vim.filetype.match({ filename = path, buf = bufnr }) local ft = vim.filetype.match({ filename = path, buf = bufnr })
if ft and ft ~= "" and vim.treesitter.language.get_lang then if ft and ft ~= '' and vim.treesitter.language.get_lang then
local lang = vim.treesitter.language.get_lang(ft) local lang = vim.treesitter.language.get_lang(ft)
-- selene: allow(empty_if)
if not pcall(vim.treesitter.start, bufnr, lang) then if not pcall(vim.treesitter.start, bufnr, lang) then
vim.bo[bufnr].syntax = ft vim.bo[bufnr].syntax = ft
else else
@ -1000,8 +1003,8 @@ M.read_file_to_scratch_buffer = function(path, preview_method)
end end
-- Replace the scratch buffer with a real buffer if we enter it -- Replace the scratch buffer with a real buffer if we enter it
vim.api.nvim_create_autocmd("BufEnter", { vim.api.nvim_create_autocmd('BufEnter', {
desc = "oil.nvim replace scratch buffer with real buffer", desc = 'oil.nvim replace scratch buffer with real buffer',
buffer = bufnr, buffer = bufnr,
callback = function() callback = function()
local winid = vim.api.nvim_get_current_win() local winid = vim.api.nvim_get_current_win()
@ -1013,7 +1016,7 @@ M.read_file_to_scratch_buffer = function(path, preview_method)
-- If we're still in a preview window, make sure this buffer still gets treated as a -- If we're still in a preview window, make sure this buffer still gets treated as a
-- preview -- preview
if vim.wo.previewwindow then if vim.wo.previewwindow then
vim.bo.bufhidden = "wipe" vim.bo.bufhidden = 'wipe'
vim.b.oil_preview_buffer = true vim.b.oil_preview_buffer = true
end end
end end
@ -1030,7 +1033,7 @@ local _regcache = {}
---@return boolean ---@return boolean
M.file_matches_bufreadcmd = function(filename) M.file_matches_bufreadcmd = function(filename)
local autocmds = vim.api.nvim_get_autocmds({ local autocmds = vim.api.nvim_get_autocmds({
event = "BufReadCmd", event = 'BufReadCmd',
}) })
for _, au in ipairs(autocmds) do for _, au in ipairs(autocmds) do
local pat = _regcache[au.pattern] local pat = _regcache[au.pattern]

View file

@ -1,12 +1,12 @@
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
local cache = require("oil.cache") local cache = require('oil.cache')
local columns = require("oil.columns") local columns = require('oil.columns')
local config = require("oil.config") local config = require('oil.config')
local constants = require("oil.constants") local constants = require('oil.constants')
local fs = require("oil.fs") local fs = require('oil.fs')
local keymap_util = require("oil.keymap_util") local keymap_util = require('oil.keymap_util')
local loading = require("oil.loading") local loading = require('oil.loading')
local util = require("oil.util") local util = require('oil.util')
local M = {} local M = {}
local FIELD_ID = constants.FIELD_ID local FIELD_ID = constants.FIELD_ID
@ -41,7 +41,7 @@ end
---Set the cursor to the last_cursor_entry if one exists ---Set the cursor to the last_cursor_entry if one exists
M.maybe_set_cursor = function() M.maybe_set_cursor = function()
local oil = require("oil") local oil = require('oil')
local bufname = vim.api.nvim_buf_get_name(0) local bufname = vim.api.nvim_buf_get_name(0)
local entry_name = last_cursor_entry[bufname] local entry_name = last_cursor_entry[bufname]
if not entry_name then if not entry_name then
@ -52,7 +52,7 @@ M.maybe_set_cursor = function()
local entry = oil.get_entry_on_line(0, lnum) local entry = oil.get_entry_on_line(0, lnum)
if entry and entry.name == entry_name then if entry and entry.name == entry_name then
local line = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1] local line = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1]
local id_str = line:match("^/(%d+)") local id_str = line:match('^/(%d+)')
local col = line:find(entry_name, 1, true) or (id_str:len() + 1) local col = line:find(entry_name, 1, true) or (id_str:len() + 1)
vim.api.nvim_win_set_cursor(0, { lnum, col - 1 }) vim.api.nvim_win_set_cursor(0, { lnum, col - 1 })
M.set_last_cursor(bufname, nil) M.set_last_cursor(bufname, nil)
@ -78,14 +78,14 @@ local function are_any_modified()
end end
local function is_unix_executable(entry) local function is_unix_executable(entry)
if entry[FIELD_TYPE] == "directory" then if entry[FIELD_TYPE] == 'directory' then
return false return false
end end
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
if not meta or not meta.stat then if not meta or not meta.stat then
return false return false
end end
if meta.stat.type == "directory" then if meta.stat.type == 'directory' then
return false return false
end end
@ -98,7 +98,7 @@ end
M.toggle_hidden = function() M.toggle_hidden = function()
local any_modified = are_any_modified() local any_modified = are_any_modified()
if any_modified then if any_modified then
vim.notify("Cannot toggle hidden files when you have unsaved changes", vim.log.levels.WARN) vim.notify('Cannot toggle hidden files when you have unsaved changes', vim.log.levels.WARN)
else else
config.view_options.show_hidden = not config.view_options.show_hidden config.view_options.show_hidden = not config.view_options.show_hidden
M.rerender_all_oil_buffers({ refetch = false }) M.rerender_all_oil_buffers({ refetch = false })
@ -109,7 +109,7 @@ end
M.set_is_hidden_file = function(is_hidden_file) M.set_is_hidden_file = function(is_hidden_file)
local any_modified = are_any_modified() local any_modified = are_any_modified()
if any_modified then if any_modified then
vim.notify("Cannot change is_hidden_file when you have unsaved changes", vim.log.levels.WARN) vim.notify('Cannot change is_hidden_file when you have unsaved changes', vim.log.levels.WARN)
else else
config.view_options.is_hidden_file = is_hidden_file config.view_options.is_hidden_file = is_hidden_file
M.rerender_all_oil_buffers({ refetch = false }) M.rerender_all_oil_buffers({ refetch = false })
@ -119,7 +119,7 @@ end
M.set_columns = function(cols) M.set_columns = function(cols)
local any_modified = are_any_modified() local any_modified = are_any_modified()
if any_modified then if any_modified then
vim.notify("Cannot change columns when you have unsaved changes", vim.log.levels.WARN) vim.notify('Cannot change columns when you have unsaved changes', vim.log.levels.WARN)
else else
config.columns = cols config.columns = cols
-- TODO only refetch if we don't have all the necessary data for the columns -- TODO only refetch if we don't have all the necessary data for the columns
@ -130,7 +130,7 @@ end
M.set_sort = function(new_sort) M.set_sort = function(new_sort)
local any_modified = are_any_modified() local any_modified = are_any_modified()
if any_modified then if any_modified then
vim.notify("Cannot change sorting when you have unsaved changes", vim.log.levels.WARN) vim.notify('Cannot change sorting when you have unsaved changes', vim.log.levels.WARN)
else else
config.view_options.sort = new_sort config.view_options.sort = new_sort
-- TODO only refetch if we don't have all the necessary data for the columns -- TODO only refetch if we don't have all the necessary data for the columns
@ -207,14 +207,14 @@ M.set_win_options = function()
local winid = vim.api.nvim_get_current_win() local winid = vim.api.nvim_get_current_win()
-- work around https://github.com/neovim/neovim/pull/27422 -- work around https://github.com/neovim/neovim/pull/27422
vim.api.nvim_set_option_value("foldmethod", "manual", { scope = "local", win = winid }) vim.api.nvim_set_option_value('foldmethod', 'manual', { scope = 'local', win = winid })
for k, v in pairs(config.win_options) do for k, v in pairs(config.win_options) do
vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) vim.api.nvim_set_option_value(k, v, { scope = 'local', win = winid })
end end
if vim.wo[winid].previewwindow then -- apply preview window options last if vim.wo[winid].previewwindow then -- apply preview window options last
for k, v in pairs(config.preview_win.win_options) do for k, v in pairs(config.preview_win.win_options) do
vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) vim.api.nvim_set_option_value(k, v, { scope = 'local', win = winid })
end end
end end
end end
@ -251,7 +251,7 @@ M.delete_hidden_buffers = function()
not visible_buffers not visible_buffers
or not hidden_buffers or not hidden_buffers
or not vim.tbl_isempty(visible_buffers) or not vim.tbl_isempty(visible_buffers)
or vim.fn.win_gettype() == "command" or vim.fn.win_gettype() == 'command'
then then
return return
end end
@ -283,15 +283,15 @@ end
--- @param cur integer[] --- @param cur integer[]
--- @return integer[] | nil --- @return integer[] | nil
local function calc_constrained_cursor_pos(bufnr, adapter, mode, cur) 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 line = vim.api.nvim_buf_get_lines(bufnr, 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
local min_col local min_col
if mode == "editable" then if mode == 'editable' then
min_col = get_first_mutable_column_col(adapter, result.ranges) min_col = get_first_mutable_column_col(adapter, result.ranges)
elseif mode == "name" then elseif mode == 'name' then
min_col = result.ranges.name[1] min_col = result.ranges.name[1]
else else
error(string.format('Unexpected value "%s" for option constrain_cursor', mode)) error(string.format('Unexpected value "%s" for option constrain_cursor', mode))
@ -318,7 +318,7 @@ local function constrain_cursor(bufnr, mode)
return return
end end
local mc = package.loaded["multicursor-nvim"] local mc = package.loaded['multicursor-nvim']
if mc then if mc then
mc.onSafeState(function() mc.onSafeState(function()
mc.action(function(ctx) mc.action(function(ctx)
@ -346,14 +346,14 @@ local function redraw_trash_virtual_text(bufnr)
if not vim.api.nvim_buf_is_valid(bufnr) or not vim.api.nvim_buf_is_loaded(bufnr) then if not vim.api.nvim_buf_is_valid(bufnr) or not vim.api.nvim_buf_is_loaded(bufnr) then
return return
end end
local parser = require("oil.mutator.parser") local parser = require('oil.mutator.parser')
local adapter = util.get_adapter(bufnr, true) 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
local _, buf_path = util.parse_url(vim.api.nvim_buf_get_name(bufnr)) local _, buf_path = util.parse_url(vim.api.nvim_buf_get_name(bufnr))
local os_path = fs.posix_to_os_path(assert(buf_path)) local os_path = fs.posix_to_os_path(assert(buf_path))
local ns = vim.api.nvim_create_namespace("OilVtext") local ns = vim.api.nvim_create_namespace('OilVtext')
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
local column_defs = columns.get_supported_columns(adapter) local column_defs = columns.get_supported_columns(adapter)
for lnum, line in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)) do for lnum, line in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)) do
@ -367,8 +367,8 @@ local function redraw_trash_virtual_text(bufnr)
vim.api.nvim_buf_set_extmark(bufnr, ns, lnum - 1, 0, { vim.api.nvim_buf_set_extmark(bufnr, ns, lnum - 1, 0, {
virt_text = { virt_text = {
{ {
"" .. fs.shorten_path(trash_info.original_path, os_path), '' .. fs.shorten_path(trash_info.original_path, os_path),
"OilTrashSourcePath", 'OilTrashSourcePath',
}, },
}, },
}) })
@ -387,13 +387,13 @@ M.initialize = function(bufnr)
end end
vim.api.nvim_clear_autocmds({ vim.api.nvim_clear_autocmds({
buffer = bufnr, buffer = bufnr,
group = "Oil", group = 'Oil',
}) })
vim.bo[bufnr].buftype = "acwrite" vim.bo[bufnr].buftype = 'acwrite'
vim.bo[bufnr].readonly = false vim.bo[bufnr].readonly = false
vim.bo[bufnr].swapfile = false vim.bo[bufnr].swapfile = false
vim.bo[bufnr].syntax = "oil" vim.bo[bufnr].syntax = 'oil'
vim.bo[bufnr].filetype = "oil" vim.bo[bufnr].filetype = 'oil'
vim.b[bufnr].EditorConfig_disable = 1 vim.b[bufnr].EditorConfig_disable = 1
session[bufnr] = session[bufnr] or {} session[bufnr] = session[bufnr] or {}
for k, v in pairs(config.buf_options) do for k, v in pairs(config.buf_options) do
@ -401,9 +401,9 @@ M.initialize = function(bufnr)
end end
vim.api.nvim_buf_call(bufnr, M.set_win_options) vim.api.nvim_buf_call(bufnr, M.set_win_options)
vim.api.nvim_create_autocmd("BufHidden", { vim.api.nvim_create_autocmd('BufHidden', {
desc = "Delete oil buffers when no longer in use", desc = 'Delete oil buffers when no longer in use',
group = "Oil", group = 'Oil',
nested = true, nested = true,
buffer = bufnr, buffer = bufnr,
callback = function() callback = function()
@ -413,7 +413,7 @@ M.initialize = function(bufnr)
-- Only delete oil buffers if none of them are visible -- Only delete oil buffers if none of them are visible
if visible_buffers and vim.tbl_isempty(visible_buffers) then if visible_buffers and vim.tbl_isempty(visible_buffers) then
-- Check if cleanup is enabled -- Check if cleanup is enabled
if type(config.cleanup_delay_ms) == "number" then if type(config.cleanup_delay_ms) == 'number' then
if config.cleanup_delay_ms > 0 then if config.cleanup_delay_ms > 0 then
vim.defer_fn(function() vim.defer_fn(function()
M.delete_hidden_buffers() M.delete_hidden_buffers()
@ -426,8 +426,8 @@ M.initialize = function(bufnr)
end, 100) end, 100)
end, end,
}) })
vim.api.nvim_create_autocmd("BufUnload", { vim.api.nvim_create_autocmd('BufUnload', {
group = "Oil", group = 'Oil',
nested = true, nested = true,
once = true, once = true,
buffer = bufnr, buffer = bufnr,
@ -439,8 +439,8 @@ M.initialize = function(bufnr)
end end
end, end,
}) })
vim.api.nvim_create_autocmd("BufEnter", { vim.api.nvim_create_autocmd('BufEnter', {
group = "Oil", group = 'Oil',
buffer = bufnr, buffer = bufnr,
callback = function(args) callback = function(args)
local opts = vim.b[args.buf].oil_dirty local opts = vim.b[args.buf].oil_dirty
@ -451,9 +451,9 @@ M.initialize = function(bufnr)
end, end,
}) })
local timer local timer
vim.api.nvim_create_autocmd("InsertEnter", { vim.api.nvim_create_autocmd('InsertEnter', {
desc = "Constrain oil cursor position", desc = 'Constrain oil cursor position',
group = "Oil", group = 'Oil',
buffer = bufnr, buffer = 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,
@ -461,12 +461,12 @@ M.initialize = function(bufnr)
vim.schedule_wrap(constrain_cursor)(bufnr, 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' }, {
desc = "Update oil preview window", desc = 'Update oil preview window',
group = "Oil", group = 'Oil',
buffer = bufnr, buffer = bufnr,
callback = function() callback = function()
local oil = require("oil") local oil = require('oil')
if vim.wo.previewwindow then if vim.wo.previewwindow then
return return
end end
@ -513,7 +513,7 @@ M.initialize = function(bufnr)
-- Set up a watcher that will refresh the directory -- Set up a watcher that will refresh the directory
if if
adapter adapter
and adapter.name == "files" and adapter.name == 'files'
and config.watch_for_changes and config.watch_for_changes
and not session[bufnr].fs_event and not session[bufnr].fs_event
then then
@ -532,7 +532,7 @@ M.initialize = function(bufnr)
fs_event:stop() fs_event:stop()
return return
end end
local mutator = require("oil.mutator") local mutator = require('oil.mutator')
if err or vim.bo[bufnr].modified or vim.b[bufnr].oil_dirty or mutator.is_mutating() then if err or vim.bo[bufnr].modified or vim.b[bufnr].oil_dirty or mutator.is_mutating() then
return return
end end
@ -553,11 +553,11 @@ M.initialize = function(bufnr)
end end
-- Watch for TextChanged and update the trash original path extmarks -- Watch for TextChanged and update the trash original path extmarks
if adapter and adapter.name == "trash" then if adapter and adapter.name == 'trash' then
local debounce_timer = assert(uv.new_timer()) local debounce_timer = assert(uv.new_timer())
local pending = false local pending = false
vim.api.nvim_create_autocmd("TextChanged", { vim.api.nvim_create_autocmd('TextChanged', {
desc = "Update oil virtual text of original path", desc = 'Update oil virtual text of original path',
buffer = bufnr, buffer = bufnr,
callback = function() callback = function()
-- Respond immediately to prevent flickering, the set the timer for a "cooldown period" -- Respond immediately to prevent flickering, the set the timer for a "cooldown period"
@ -583,14 +583,14 @@ M.initialize = function(bufnr)
M.render_buffer_async(bufnr, {}, function(err) M.render_buffer_async(bufnr, {}, function(err)
if err then if err then
vim.notify( vim.notify(
string.format("Error rendering oil buffer %s: %s", vim.api.nvim_buf_get_name(bufnr), err), string.format('Error rendering oil buffer %s: %s', vim.api.nvim_buf_get_name(bufnr), err),
vim.log.levels.ERROR vim.log.levels.ERROR
) )
else else
vim.b[bufnr].oil_ready = true vim.b[bufnr].oil_ready = true
vim.api.nvim_exec_autocmds( vim.api.nvim_exec_autocmds(
"User", 'User',
{ pattern = "OilEnter", modeline = false, data = { buf = bufnr } } { pattern = 'OilEnter', modeline = false, data = { buf = bufnr } }
) )
end end
end) end)
@ -606,12 +606,12 @@ local function get_sort_function(adapter, num_entries)
-- If empty, default to type + name sorting -- If empty, default to type + name sorting
if vim.tbl_isempty(sort_config) then if vim.tbl_isempty(sort_config) then
sort_config = { { "type", "asc" }, { "name", "asc" } } sort_config = { { 'type', 'asc' }, { 'name', 'asc' } }
end end
for _, sort_pair in ipairs(sort_config) do for _, sort_pair in ipairs(sort_config) do
local col_name, order = unpack(sort_pair) local col_name, order = unpack(sort_pair)
if order ~= "asc" and order ~= "desc" then if order ~= 'asc' and order ~= 'desc' then
vim.notify_once( vim.notify_once(
string.format( string.format(
"Column '%s' has invalid sort order '%s'. Should be either 'asc' or 'desc'", "Column '%s' has invalid sort order '%s'. Should be either 'asc' or 'desc'",
@ -639,7 +639,7 @@ local function get_sort_function(adapter, num_entries)
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
if order == "desc" then if order == 'desc' then
return a_val > b_val return a_val > b_val
else else
return a_val < b_val return a_val < b_val
@ -663,7 +663,7 @@ local function render_buffer(bufnr, opts)
return false return false
end end
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
opts = vim.tbl_extend("keep", opts or {}, { opts = vim.tbl_extend('keep', opts or {}, {
jump = false, jump = false,
jump_first = false, jump_first = false,
}) })
@ -693,10 +693,10 @@ local function render_buffer(bufnr, opts)
for i, col_def in ipairs(column_defs) do 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) local _, conf = util.split_config(col_def)
col_align[i + 1] = conf and conf.align or "left" col_align[i + 1] = conf and conf.align or 'left'
end end
local parent_entry = { 0, "..", "directory" } local parent_entry = { 0, '..', 'directory' }
if M.should_display(bufnr, parent_entry) then if M.should_display(bufnr, parent_entry) then
local cols = M.format_entry_cols(parent_entry, column_defs, col_width, adapter, true, bufnr) local cols = M.format_entry_cols(parent_entry, column_defs, col_width, adapter, true, bufnr)
table.insert(line_table, cols) table.insert(line_table, cols)
@ -732,7 +732,7 @@ local function render_buffer(bufnr, opts)
if jump_idx then if jump_idx then
local lnum = jump_idx local lnum = jump_idx
local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1] local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]
local id_str = line:match("^/(%d+)") local id_str = line:match('^/(%d+)')
local id = tonumber(id_str) local id = tonumber(id_str)
if id then if id then
local entry = cache.get_entry_by_id(id) local entry = cache.get_entry_by_id(id)
@ -745,7 +745,7 @@ local function render_buffer(bufnr, opts)
end end
end end
constrain_cursor(bufnr, "name") constrain_cursor(bufnr, 'name')
end end
end end
end) end)
@ -760,13 +760,13 @@ end
local function get_link_text(name, meta) local function get_link_text(name, meta)
local link_text local link_text
if meta then if meta then
if meta.link_stat and meta.link_stat.type == "directory" then if meta.link_stat and meta.link_stat.type == 'directory' then
name = name .. "/" name = name .. '/'
end end
if meta.link then if meta.link then
link_text = "-> " .. meta.link:gsub("\n", "") link_text = '-> ' .. meta.link:gsub('\n', '')
if meta.link_stat and meta.link_stat.type == "directory" then if meta.link_stat and meta.link_stat.type == 'directory' then
link_text = util.addslash(link_text) link_text = util.addslash(link_text)
end end
end end
@ -786,15 +786,15 @@ end
M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden, bufnr) M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden, bufnr)
local name = entry[FIELD_NAME] local name = entry[FIELD_NAME]
local meta = entry[FIELD_META] local meta = entry[FIELD_META]
local hl_suffix = "" local hl_suffix = ''
if is_hidden then if is_hidden then
hl_suffix = "Hidden" hl_suffix = 'Hidden'
end end
if meta and meta.display_name then if meta and meta.display_name then
name = meta.display_name name = meta.display_name
end end
-- We can't handle newlines in filenames (and shame on you for doing that) -- We can't handle newlines in filenames (and shame on you for doing that)
name = name:gsub("\n", "") name = name:gsub('\n', '')
-- First put the unique ID -- First put the unique ID
local cols = {} local cols = {}
local id_key = cache.format_id(entry[FIELD_ID]) local id_key = cache.format_id(entry[FIELD_ID])
@ -803,7 +803,7 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
-- Then add all the configured columns -- Then add all the configured columns
for i, column in ipairs(column_defs) do for i, column in ipairs(column_defs) do
local chunk = columns.render_col(adapter, column, entry, bufnr) local chunk = columns.render_col(adapter, column, entry, bufnr)
local text = type(chunk) == "table" and chunk[1] or chunk local text = type(chunk) == 'table' and chunk[1] or chunk
---@cast text string ---@cast text string
col_width[i + 1] = math.max(col_width[i + 1], vim.api.nvim_strwidth(text)) col_width[i + 1] = math.max(col_width[i + 1], vim.api.nvim_strwidth(text))
table.insert(cols, chunk) table.insert(cols, chunk)
@ -816,7 +816,7 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
if get_custom_hl then if get_custom_hl then
local external_entry = util.export_entry(entry) local external_entry = util.export_entry(entry)
if entry_type == "link" then if entry_type == 'link' then
link_name, link_target = get_link_text(name, meta) link_name, link_target = get_link_text(name, meta)
local is_orphan = not (meta and meta.link_stat) local is_orphan = not (meta and meta.link_stat)
link_name_hl = get_custom_hl(external_entry, is_hidden, false, is_orphan, bufnr) link_name_hl = get_custom_hl(external_entry, is_hidden, false, is_orphan, bufnr)
@ -830,8 +830,8 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
local hl = get_custom_hl(external_entry, is_hidden, false, false, bufnr) local hl = get_custom_hl(external_entry, is_hidden, false, false, bufnr)
if hl then if hl then
-- Add the trailing / if this is a directory, this is important -- Add the trailing / if this is a directory, this is important
if entry_type == "directory" then if entry_type == 'directory' then
name = name .. "/" name = name .. '/'
end end
table.insert(cols, { name, hl }) table.insert(cols, { name, hl })
return cols return cols
@ -840,49 +840,50 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
end end
local highlight_as_executable = false local highlight_as_executable = false
if entry_type ~= "directory" then if entry_type ~= 'directory' then
local lower = name:lower() local lower = name:lower()
if if
lower:match("%.exe$") lower:match('%.exe$')
or lower:match("%.bat$") or lower:match('%.bat$')
or lower:match("%.cmd$") or lower:match('%.cmd$')
or lower:match("%.com$") or lower:match('%.com$')
or lower:match("%.ps1$") or lower:match('%.ps1$')
then then
highlight_as_executable = true highlight_as_executable = true
-- selene: allow(if_same_then_else)
elseif is_unix_executable(entry) then elseif is_unix_executable(entry) then
highlight_as_executable = true highlight_as_executable = true
end end
end end
if entry_type == "directory" then if entry_type == 'directory' then
table.insert(cols, { name .. "/", "OilDir" .. hl_suffix }) table.insert(cols, { name .. '/', 'OilDir' .. hl_suffix })
elseif entry_type == "socket" then elseif entry_type == 'socket' then
table.insert(cols, { name, "OilSocket" .. hl_suffix }) table.insert(cols, { name, 'OilSocket' .. hl_suffix })
elseif entry_type == "link" then elseif entry_type == 'link' then
if not link_name then if not link_name then
link_name, link_target = get_link_text(name, meta) link_name, link_target = get_link_text(name, meta)
end end
local is_orphan = not (meta and meta.link_stat) local is_orphan = not (meta and meta.link_stat)
if not link_name_hl then if not link_name_hl then
if highlight_as_executable then if highlight_as_executable then
link_name_hl = "OilExecutable" .. hl_suffix link_name_hl = 'OilExecutable' .. hl_suffix
else else
link_name_hl = (is_orphan and "OilOrphanLink" or "OilLink") .. hl_suffix link_name_hl = (is_orphan and 'OilOrphanLink' or 'OilLink') .. hl_suffix
end end
end end
table.insert(cols, { link_name, link_name_hl }) table.insert(cols, { link_name, link_name_hl })
if link_target then if link_target then
if not link_target_hl then if not link_target_hl then
link_target_hl = (is_orphan and "OilOrphanLinkTarget" or "OilLinkTarget") .. hl_suffix link_target_hl = (is_orphan and 'OilOrphanLinkTarget' or 'OilLinkTarget') .. hl_suffix
end end
table.insert(cols, { link_target, link_target_hl }) table.insert(cols, { link_target, link_target_hl })
end end
elseif highlight_as_executable then elseif highlight_as_executable then
table.insert(cols, { name, "OilExecutable" .. hl_suffix }) table.insert(cols, { name, 'OilExecutable' .. hl_suffix })
else else
table.insert(cols, { name, "OilFile" .. hl_suffix }) table.insert(cols, { name, 'OilFile' .. hl_suffix })
end end
return cols return cols
@ -914,8 +915,8 @@ M.render_buffer_async = function(bufnr, opts, caller_callback)
local function callback(err) local function callback(err)
if not err then if not err then
vim.api.nvim_exec_autocmds( vim.api.nvim_exec_autocmds(
"User", 'User',
{ pattern = "OilReadPost", modeline = false, data = { buf = bufnr } } { pattern = 'OilReadPost', modeline = false, data = { buf = bufnr } }
) )
end end
if caller_callback then if caller_callback then
@ -923,7 +924,7 @@ M.render_buffer_async = function(bufnr, opts, caller_callback)
end end
end end
opts = vim.tbl_deep_extend("keep", opts or {}, { opts = vim.tbl_deep_extend('keep', opts or {}, {
refetch = true, refetch = true,
}) })
---@cast opts -nil ---@cast opts -nil
@ -949,8 +950,8 @@ M.render_buffer_async = function(bufnr, opts, caller_callback)
vim.bo[bufnr].undolevels = -1 vim.bo[bufnr].undolevels = -1
local handle_error = vim.schedule_wrap(function(message) local handle_error = vim.schedule_wrap(function(message)
vim.b[bufnr].oil_rendering = false vim.b[bufnr].oil_rendering = false
vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" }) vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value('undolevels', { scope = 'global' })
util.render_text(bufnr, { "Error: " .. message }) util.render_text(bufnr, { 'Error: ' .. message })
if pending_renders[bufnr] then if pending_renders[bufnr] then
for _, cb in ipairs(pending_renders[bufnr]) do for _, cb in ipairs(pending_renders[bufnr]) do
cb(message) cb(message)
@ -987,7 +988,7 @@ M.render_buffer_async = function(bufnr, opts, caller_callback)
loading.set_loading(bufnr, false) loading.set_loading(bufnr, false)
render_buffer(bufnr, { jump = true }) render_buffer(bufnr, { jump = true })
M.set_last_cursor(bufname, nil) M.set_last_cursor(bufname, nil)
vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" }) vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value('undolevels', { scope = 'global' })
vim.bo[bufnr].modifiable = not buffers_locked and adapter.is_modifiable(bufnr) vim.bo[bufnr].modifiable = not buffers_locked and adapter.is_modifiable(bufnr)
if callback then if callback then
callback() callback()

View file

@ -1,7 +1,7 @@
local M = {} local M = {}
M.is_win_supported = function(winid, bufnr) M.is_win_supported = function(winid, bufnr)
return vim.bo[bufnr].filetype == "oil" return vim.bo[bufnr].filetype == 'oil'
end end
M.save_win = function(winid) M.save_win = function(winid)
@ -11,7 +11,7 @@ M.save_win = function(winid)
end end
M.load_win = function(winid, config) M.load_win = function(winid, config)
require("oil").open(config.bufname) require('oil').open(config.bufname)
end end
return M return M

30
oil.nvim-scm-1.rockspec Normal file
View file

@ -0,0 +1,30 @@
rockspec_format = '3.0'
package = 'oil.nvim'
version = 'scm-1'
source = {
url = 'git+https://github.com/barrettruth/oil.nvim.git',
}
description = {
summary = 'Neovim file explorer: edit your filesystem like a buffer',
homepage = 'https://github.com/barrettruth/oil.nvim',
license = 'MIT',
}
dependencies = {
'lua >= 5.1',
}
test_dependencies = {
'nlua',
'busted >= 2.1.1',
}
test = {
type = 'busted',
}
build = {
type = 'builtin',
}

View file

@ -1,7 +1,7 @@
vim.opt.runtimepath:prepend("scripts/benchmark.nvim") vim.opt.runtimepath:prepend('scripts/benchmark.nvim')
vim.opt.runtimepath:prepend(".") vim.opt.runtimepath:prepend('.')
local bm = require("benchmark") local bm = require('benchmark')
bm.sandbox() bm.sandbox()
---@module 'oil' ---@module 'oil'
@ -14,50 +14,53 @@ local DIR_SIZE = tonumber(vim.env.DIR_SIZE) or 100000
local ITERATIONS = tonumber(vim.env.ITERATIONS) or 10 local ITERATIONS = tonumber(vim.env.ITERATIONS) or 10
local WARM_UP = tonumber(vim.env.WARM_UP) or 1 local WARM_UP = tonumber(vim.env.WARM_UP) or 1
local OUTLIERS = tonumber(vim.env.OUTLIERS) or math.floor(ITERATIONS / 10) local OUTLIERS = tonumber(vim.env.OUTLIERS) or math.floor(ITERATIONS / 10)
local TEST_DIR = "perf/tmp/test_" .. DIR_SIZE local TEST_DIR = 'perf/tmp/test_' .. DIR_SIZE
vim.fn.mkdir(TEST_DIR, "p") vim.fn.mkdir(TEST_DIR, 'p')
require("benchmark.files").create_files(TEST_DIR, "file %d.txt", DIR_SIZE) require('benchmark.files').create_files(TEST_DIR, 'file %d.txt', DIR_SIZE)
-- selene: allow(global_usage)
function _G.jit_profile() function _G.jit_profile()
require("oil").setup(setup_opts) require('oil').setup(setup_opts)
local finish = bm.jit_profile({ filename = TEST_DIR .. "/profile.txt" }) local finish = bm.jit_profile({ filename = TEST_DIR .. '/profile.txt' })
bm.wait_for_user_event("OilEnter", function() bm.wait_for_user_event('OilEnter', function()
finish() finish()
end) end)
require("oil").open(TEST_DIR) require('oil').open(TEST_DIR)
end end
-- selene: allow(global_usage)
function _G.flame_profile() function _G.flame_profile()
local start, stop = bm.flame_profile({ local start, stop = bm.flame_profile({
pattern = "oil*", pattern = 'oil*',
filename = "profile.json", filename = 'profile.json',
}) })
require("oil").setup(setup_opts) require('oil').setup(setup_opts)
start() start()
bm.wait_for_user_event("OilEnter", function() bm.wait_for_user_event('OilEnter', function()
stop(function() stop(function()
vim.cmd.qall({ mods = { silent = true } }) vim.cmd.qall({ mods = { silent = true } })
end) end)
end) end)
require("oil").open(TEST_DIR) require('oil').open(TEST_DIR)
end end
-- selene: allow(global_usage)
function _G.benchmark() function _G.benchmark()
require("oil").setup(setup_opts) require('oil').setup(setup_opts)
bm.run({ title = "oil.nvim", iterations = ITERATIONS, warm_up = WARM_UP }, function(callback) bm.run({ title = 'oil.nvim', iterations = ITERATIONS, warm_up = WARM_UP }, function(callback)
bm.wait_for_user_event("OilEnter", callback) bm.wait_for_user_event('OilEnter', callback)
require("oil").open(TEST_DIR) require('oil').open(TEST_DIR)
end, function(times) end, function(times)
local avg = bm.avg(times, { trim_outliers = OUTLIERS }) local avg = bm.avg(times, { trim_outliers = OUTLIERS })
local std_dev = bm.std_dev(times, { trim_outliers = OUTLIERS }) local std_dev = bm.std_dev(times, { trim_outliers = OUTLIERS })
local lines = { local lines = {
table.concat(vim.tbl_map(bm.format_time, times), " "), table.concat(vim.tbl_map(bm.format_time, times), ' '),
string.format("Average: %s", bm.format_time(avg)), string.format('Average: %s', bm.format_time(avg)),
string.format("Std deviation: %s", bm.format_time(std_dev)), string.format('Std deviation: %s', bm.format_time(std_dev)),
} }
vim.fn.writefile(lines, "perf/tmp/benchmark.txt") vim.fn.writefile(lines, 'perf/tmp/benchmark.txt')
vim.cmd.qall({ mods = { silent = true } }) vim.cmd.qall({ mods = { silent = true } })
end) end)
end end

View file

@ -1,3 +1,3 @@
if vim.g.oil ~= nil then if vim.g.oil ~= nil then
require("oil").setup() require('oil').setup()
end end

View file

@ -1,24 +0,0 @@
#!/usr/bin/env bash
set -e
mkdir -p ".testenv/config/nvim"
mkdir -p ".testenv/data/nvim"
mkdir -p ".testenv/state/nvim"
mkdir -p ".testenv/run/nvim"
mkdir -p ".testenv/cache/nvim"
PLUGINS=".testenv/data/nvim/site/pack/plugins/start"
if [ ! -e "$PLUGINS/plenary.nvim" ]; then
git clone --depth=1 https://github.com/nvim-lua/plenary.nvim.git "$PLUGINS/plenary.nvim"
else
(cd "$PLUGINS/plenary.nvim" && git pull)
fi
XDG_CONFIG_HOME=".testenv/config" \
XDG_DATA_HOME=".testenv/data" \
XDG_STATE_HOME=".testenv/state" \
XDG_RUNTIME_DIR=".testenv/run" \
XDG_CACHE_HOME=".testenv/cache" \
nvim --headless -u tests/minimal_init.lua \
-c "PlenaryBustedDirectory ${1-tests} { minimal_init = './tests/minimal_init.lua' }"
echo "Success"

5
selene.toml Normal file
View file

@ -0,0 +1,5 @@
std = 'vim'
[lints]
mixed_table = 'allow'
unused_variable = 'allow'

49
spec/TESTING.md Normal file
View file

@ -0,0 +1,49 @@
# Test Framework Migration Log
Issues encountered during the plenary-to-busted migration (`6be0148`) and
how they were resolved.
## Final status
114 successes / 0 failures / 0 errors.
## Issue 1: altbuf_spec.lua:18 — BufEnter wait mismatch (resolved)
The test `sets previous buffer as alternate when editing url file` originally
waited for two `BufEnter` events via `wait_for_autocmd('BufEnter')` x2.
The oil:// → real file resolution only produces one async BufEnter:
1. `vim.cmd.edit('oil://...')` fires BufEnter synchronously (before any
wait_for_autocmd is registered)
2. `normalize_url` resolves asynchronously via `uv.fs_realpath` + `uv.fs_stat`
3. `rename_buffer` discovers dest is a real file on disk, schedules
`nvim_win_set_buf()` via `vim.schedule()`
4. `nvim_win_set_buf()` fires one async BufEnter on the real file buffer
The second `wait_for_autocmd('BufEnter')` was never correct — it passed under
plenary due to coroutine event loop yielding, not because a second BufEnter
actually fired.
**Fix:** single `wait_for_autocmd('BufEnter')`.
## Issue 2: regression_spec.lua:20 — stale preview window state (resolved)
`reset_editor()` closed all windows except the first, but the surviving window
could be a preview window with stale `oil_preview`, `oil_source_win`,
`previewwindow`, etc. from a prior test. Subsequent tests that triggered
`close_preview_window_if_not_in_oil()` would close the wrong window.
**Fix:** `vim.cmd.new()` + `vim.cmd.only()` creates a fresh window and closes
all others (including the stale one). No variable cleanup needed — the new
window has no oil state.
## Issue 3: preview_spec.lua:30 — M.await timed out (resolved)
Originally thought to be a pre-existing issue unrelated to the migration. Root
cause was identical to issue 2: stale preview window state from a prior test
caused `close_preview_window_if_not_in_oil()` to close the wrong window during
`oil.open()`, preventing the callback from completing.
Fixed by the same `vim.cmd.new()` + `vim.cmd.only()` approach in
`reset_editor()`.

150
spec/altbuf_spec.lua Normal file
View file

@ -0,0 +1,150 @@
local fs = require('oil.fs')
local oil = require('oil')
local test_util = require('spec.test_util')
describe('Alternate buffer', function()
after_each(function()
test_util.reset_editor()
end)
it('sets previous buffer as alternate', function()
vim.cmd.edit({ args = { 'foo' } })
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
vim.cmd.edit({ args = { 'bar' } })
assert.equals('foo', vim.fn.expand('#'))
end)
it('sets previous buffer as alternate when editing url file', function()
vim.cmd.edit({ args = { 'foo' } })
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
local readme = fs.join(vim.fn.getcwd(), 'README.md')
vim.cmd.edit({ args = { 'oil://' .. fs.os_to_posix_path(readme) } })
test_util.wait_for_autocmd('BufEnter')
assert.equals(readme, vim.api.nvim_buf_get_name(0))
assert.equals('foo', vim.fn.expand('#'))
end)
it('sets previous buffer as alternate when editing oil://', function()
vim.cmd.edit({ args = { 'foo' } })
vim.cmd.edit({ args = { 'oil://' .. fs.os_to_posix_path(vim.fn.getcwd()) } })
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
vim.cmd.edit({ args = { 'bar' } })
assert.equals('foo', vim.fn.expand('#'))
end)
it('preserves alternate buffer if editing the same file', function()
vim.cmd.edit({ args = { 'foo' } })
vim.cmd.edit({ args = { 'bar' } })
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
vim.cmd.edit({ args = { 'bar' } })
assert.equals('foo', vim.fn.expand('#'))
end)
it('preserves alternate buffer if discarding changes', function()
vim.cmd.edit({ args = { 'foo' } })
vim.cmd.edit({ args = { 'bar' } })
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
oil.close()
assert.equals('bar', vim.fn.expand('%'))
assert.equals('foo', vim.fn.expand('#'))
end)
it('sets previous buffer as alternate after multi-dir hops', function()
vim.cmd.edit({ args = { 'foo' } })
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
vim.cmd.edit({ args = { 'bar' } })
assert.equals('foo', vim.fn.expand('#'))
end)
it('sets previous buffer as alternate when inside oil buffer', function()
vim.cmd.edit({ args = { 'foo' } })
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
assert.equals('foo', vim.fn.expand('#'))
vim.cmd.edit({ args = { 'bar' } })
assert.equals('foo', vim.fn.expand('#'))
oil.open()
assert.equals('bar', vim.fn.expand('#'))
end)
it('preserves alternate when traversing oil dirs', function()
vim.cmd.edit({ args = { 'foo' } })
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
assert.equals('foo', vim.fn.expand('#'))
vim.wait(1000, function()
return oil.get_cursor_entry()
end, 10)
vim.api.nvim_win_set_cursor(0, { 1, 1 })
oil.select()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
assert.equals('foo', vim.fn.expand('#'))
end)
it('preserves alternate when opening preview', function()
vim.cmd.edit({ args = { 'foo' } })
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
assert.equals('foo', vim.fn.expand('#'))
vim.wait(1000, function()
return oil.get_cursor_entry()
end, 10)
vim.api.nvim_win_set_cursor(0, { 1, 1 })
oil.open_preview()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
assert.equals('foo', vim.fn.expand('#'))
end)
describe('floating window', function()
it('sets previous buffer as alternate', function()
vim.cmd.edit({ args = { 'foo' } })
oil.open_float()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
vim.api.nvim_win_close(0, true)
vim.cmd.edit({ args = { 'bar' } })
assert.equals('foo', vim.fn.expand('#'))
end)
it('preserves alternate buffer if editing the same file', function()
vim.cmd.edit({ args = { 'foo' } })
vim.cmd.edit({ args = { 'bar' } })
oil.open_float()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
vim.api.nvim_win_close(0, true)
vim.cmd.edit({ args = { 'bar' } })
assert.equals('foo', vim.fn.expand('#'))
end)
it('preserves alternate buffer if discarding changes', function()
vim.cmd.edit({ args = { 'foo' } })
vim.cmd.edit({ args = { 'bar' } })
oil.open_float()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
oil.close()
assert.equals('foo', vim.fn.expand('#'))
end)
it('preserves alternate when traversing to a new file', function()
vim.cmd.edit({ args = { 'foo' } })
oil.open_float()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
assert.equals('foo', vim.fn.expand('#'))
test_util.feedkeys({ '/LICENSE<CR>' }, 10)
oil.select()
test_util.wait_for_autocmd('BufEnter')
assert.equals('LICENSE', vim.fn.expand('%:.'))
assert.equals('foo', vim.fn.expand('#'))
end)
end)
end)

44
spec/close_spec.lua Normal file
View file

@ -0,0 +1,44 @@
local oil = require('oil')
local test_util = require('spec.test_util')
describe('close', function()
before_each(function()
test_util.reset_editor()
end)
after_each(function()
test_util.reset_editor()
end)
it('does not close buffer from visual mode', function()
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
assert.equals('oil', vim.bo.filetype)
test_util.feedkeys({ 'V' }, 10)
oil.close()
assert.equals('oil', vim.bo.filetype)
test_util.feedkeys({ '<Esc>' }, 10)
end)
it('does not close buffer from operator-pending mode', function()
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
assert.equals('oil', vim.bo.filetype)
vim.api.nvim_feedkeys('d', 'n', false)
vim.wait(20)
local mode = vim.api.nvim_get_mode().mode
if mode:match('^no') then
oil.close()
assert.equals('oil', vim.bo.filetype)
end
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<Esc>', true, true, true), 'n', false)
vim.wait(20)
end)
it('closes buffer from normal mode', function()
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
assert.equals('oil', vim.bo.filetype)
oil.close()
assert.not_equals('oil', vim.bo.filetype)
end)
end)

View file

@ -1,25 +1,25 @@
local config = require("oil.config") local config = require('oil.config')
describe("config", function() describe('config', function()
after_each(function() after_each(function()
vim.g.oil = nil vim.g.oil = nil
end) end)
it("falls back to vim.g.oil when setup() is called with no args", function() it('falls back to vim.g.oil when setup() is called with no args', function()
vim.g.oil = { delete_to_trash = true, cleanup_delay_ms = 5000 } vim.g.oil = { delete_to_trash = true, cleanup_delay_ms = 5000 }
config.setup() config.setup()
assert.is_true(config.delete_to_trash) assert.is_true(config.delete_to_trash)
assert.equals(5000, config.cleanup_delay_ms) assert.equals(5000, config.cleanup_delay_ms)
end) end)
it("uses defaults when neither opts nor vim.g.oil is set", function() it('uses defaults when neither opts nor vim.g.oil is set', function()
vim.g.oil = nil vim.g.oil = nil
config.setup() config.setup()
assert.is_false(config.delete_to_trash) assert.is_false(config.delete_to_trash)
assert.equals(2000, config.cleanup_delay_ms) assert.equals(2000, config.cleanup_delay_ms)
end) end)
it("prefers explicit opts over vim.g.oil", function() it('prefers explicit opts over vim.g.oil', function()
vim.g.oil = { delete_to_trash = true } vim.g.oil = { delete_to_trash = true }
config.setup({ delete_to_trash = false }) config.setup({ delete_to_trash = false })
assert.is_false(config.delete_to_trash) assert.is_false(config.delete_to_trash)

166
spec/files_spec.lua Normal file
View file

@ -0,0 +1,166 @@
local TmpDir = require('spec.tmpdir')
local files = require('oil.adapters.files')
local test_util = require('spec.test_util')
describe('files adapter', function()
local tmpdir
before_each(function()
tmpdir = TmpDir.new()
end)
after_each(function()
if tmpdir then
tmpdir:dispose()
end
test_util.reset_editor()
end)
it('tmpdir creates files and asserts they exist', function()
tmpdir:create({ 'a.txt', 'foo/b.txt', 'foo/c.txt', 'bar/' })
tmpdir:assert_fs({
['a.txt'] = 'a.txt',
['foo/b.txt'] = 'foo/b.txt',
['foo/c.txt'] = 'foo/c.txt',
['bar/'] = true,
})
end)
it('Creates files', function()
local err = test_util.await(files.perform_action, 2, {
url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a.txt',
entry_type = 'file',
type = 'create',
})
assert.is_nil(err)
tmpdir:assert_fs({
['a.txt'] = '',
})
end)
it('Creates directories', function()
local err = test_util.await(files.perform_action, 2, {
url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a',
entry_type = 'directory',
type = 'create',
})
assert.is_nil(err)
tmpdir:assert_fs({
['a/'] = true,
})
end)
it('Deletes files', function()
tmpdir:create({ 'a.txt' })
local url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a.txt'
local err = test_util.await(files.perform_action, 2, {
url = url,
entry_type = 'file',
type = 'delete',
})
assert.is_nil(err)
tmpdir:assert_fs({})
end)
it('Deletes directories', function()
tmpdir:create({ 'a/' })
local url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a'
local err = test_util.await(files.perform_action, 2, {
url = url,
entry_type = 'directory',
type = 'delete',
})
assert.is_nil(err)
tmpdir:assert_fs({})
end)
it('Moves files', function()
tmpdir:create({ 'a.txt' })
local src_url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a.txt'
local dest_url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'b.txt'
local err = test_util.await(files.perform_action, 2, {
src_url = src_url,
dest_url = dest_url,
entry_type = 'file',
type = 'move',
})
assert.is_nil(err)
tmpdir:assert_fs({
['b.txt'] = 'a.txt',
})
end)
it('Moves directories', function()
tmpdir:create({ 'a/a.txt' })
local src_url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a'
local dest_url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'b'
local err = test_util.await(files.perform_action, 2, {
src_url = src_url,
dest_url = dest_url,
entry_type = 'directory',
type = 'move',
})
assert.is_nil(err)
tmpdir:assert_fs({
['b/a.txt'] = 'a/a.txt',
['b/'] = true,
})
end)
it('Copies files', function()
tmpdir:create({ 'a.txt' })
local src_url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a.txt'
local dest_url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'b.txt'
local err = test_util.await(files.perform_action, 2, {
src_url = src_url,
dest_url = dest_url,
entry_type = 'file',
type = 'copy',
})
assert.is_nil(err)
tmpdir:assert_fs({
['a.txt'] = 'a.txt',
['b.txt'] = 'a.txt',
})
end)
it('Recursively copies directories', function()
tmpdir:create({ 'a/a.txt' })
local src_url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a'
local dest_url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'b'
local err = test_util.await(files.perform_action, 2, {
src_url = src_url,
dest_url = dest_url,
entry_type = 'directory',
type = 'copy',
})
assert.is_nil(err)
tmpdir:assert_fs({
['b/a.txt'] = 'a/a.txt',
['b/'] = true,
['a/a.txt'] = 'a/a.txt',
['a/'] = true,
})
end)
it('Editing a new oil://path/ creates an oil buffer', function()
local tmpdir_url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. '/'
vim.cmd.edit({ args = { tmpdir_url } })
test_util.wait_oil_ready()
local new_url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'newdir'
vim.cmd.edit({ args = { new_url } })
test_util.wait_oil_ready()
assert.equals('oil', vim.bo.filetype)
assert.equals(new_url .. '/', vim.api.nvim_buf_get_name(0))
end)
it('Editing a new oil://file.rb creates a normal buffer', function()
local tmpdir_url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. '/'
vim.cmd.edit({ args = { tmpdir_url } })
test_util.wait_for_autocmd('BufReadPost')
local new_url = 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'file.rb'
vim.cmd.edit({ args = { new_url } })
test_util.wait_for_autocmd('BufReadPost')
assert.equals('ruby', vim.bo.filetype)
assert.equals(vim.fn.fnamemodify(tmpdir.path, ':p') .. 'file.rb', vim.api.nvim_buf_get_name(0))
assert.equals(tmpdir.path .. '/file.rb', vim.fn.bufname())
end)
end)

View file

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

9
spec/minimal_init.lua Normal file
View file

@ -0,0 +1,9 @@
vim.cmd([[set runtimepath=$VIMRUNTIME]])
vim.opt.runtimepath:append('.')
vim.opt.packpath = {}
vim.o.swapfile = false
vim.cmd('filetype plugin indent on')
vim.fn.mkdir(vim.fn.stdpath('cache'), 'p')
vim.fn.mkdir(vim.fn.stdpath('data'), 'p')
vim.fn.mkdir(vim.fn.stdpath('state'), 'p')
require('spec.test_util').reset_editor()

59
spec/move_rename_spec.lua Normal file
View file

@ -0,0 +1,59 @@
local fs = require('oil.fs')
local test_util = require('spec.test_util')
local util = require('oil.util')
describe('update_moved_buffers', function()
after_each(function()
test_util.reset_editor()
end)
it('Renames moved buffers', function()
vim.cmd.edit({ args = { 'oil-test:///foo/bar.txt' } })
util.update_moved_buffers('file', 'oil-test:///foo/bar.txt', 'oil-test:///foo/baz.txt')
assert.equals('oil-test:///foo/baz.txt', vim.api.nvim_buf_get_name(0))
end)
it('Renames moved buffers when they are normal files', function()
local tmpdir = fs.join(vim.loop.fs_realpath(vim.fn.stdpath('cache')), 'oil', 'test')
local testfile = fs.join(tmpdir, 'foo.txt')
vim.cmd.edit({ args = { testfile } })
util.update_moved_buffers(
'file',
'oil://' .. fs.os_to_posix_path(testfile),
'oil://' .. fs.os_to_posix_path(fs.join(tmpdir, 'bar.txt'))
)
assert.equals(fs.join(tmpdir, 'bar.txt'), vim.api.nvim_buf_get_name(0))
end)
it('Renames directories', function()
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
util.update_moved_buffers('directory', 'oil-test:///foo/', 'oil-test:///bar/')
assert.equals('oil-test:///bar/', vim.api.nvim_buf_get_name(0))
end)
it('Renames subdirectories', function()
vim.cmd.edit({ args = { 'oil-test:///foo/bar/' } })
util.update_moved_buffers('directory', 'oil-test:///foo/', 'oil-test:///baz/')
assert.equals('oil-test:///baz/bar/', vim.api.nvim_buf_get_name(0))
end)
it('Renames subfiles', function()
vim.cmd.edit({ args = { 'oil-test:///foo/bar.txt' } })
util.update_moved_buffers('directory', 'oil-test:///foo/', 'oil-test:///baz/')
assert.equals('oil-test:///baz/bar.txt', vim.api.nvim_buf_get_name(0))
end)
it('Renames subfiles when they are normal files', function()
local tmpdir = fs.join(vim.loop.fs_realpath(vim.fn.stdpath('cache')), 'oil', 'test')
local foo = fs.join(tmpdir, 'foo')
local bar = fs.join(tmpdir, 'bar')
local testfile = fs.join(foo, 'foo.txt')
vim.cmd.edit({ args = { testfile } })
util.update_moved_buffers(
'directory',
'oil://' .. fs.os_to_posix_path(foo),
'oil://' .. fs.os_to_posix_path(bar)
)
assert.equals(fs.join(bar, 'foo.txt'), vim.api.nvim_buf_get_name(0))
end)
end)

419
spec/mutator_spec.lua Normal file
View file

@ -0,0 +1,419 @@
local cache = require('oil.cache')
local constants = require('oil.constants')
local mutator = require('oil.mutator')
local test_adapter = require('oil.adapters.test')
local test_util = require('spec.test_util')
local FIELD_ID = constants.FIELD_ID
local FIELD_NAME = constants.FIELD_NAME
local FIELD_TYPE = constants.FIELD_TYPE
describe('mutator', function()
after_each(function()
test_util.reset_editor()
end)
describe('build actions', function()
it('empty diffs produce no actions', function()
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
local actions = mutator.create_actions_from_diffs({
[bufnr] = {},
})
assert.are.same({}, actions)
end)
it('constructs CREATE actions', function()
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
local diffs = {
{ type = 'new', name = 'a.txt', entry_type = 'file' },
}
local actions = mutator.create_actions_from_diffs({
[bufnr] = diffs,
})
assert.are.same({
{
type = 'create',
entry_type = 'file',
url = 'oil-test:///foo/a.txt',
},
}, actions)
end)
it('constructs DELETE actions', function()
local file = test_adapter.test_set('/foo/a.txt', 'file')
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
local diffs = {
{ type = 'delete', name = 'a.txt', id = file[FIELD_ID] },
}
local actions = mutator.create_actions_from_diffs({
[bufnr] = diffs,
})
assert.are.same({
{
type = 'delete',
entry_type = 'file',
url = 'oil-test:///foo/a.txt',
},
}, actions)
end)
it('constructs COPY actions', function()
local file = test_adapter.test_set('/foo/a.txt', 'file')
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
local diffs = {
{ type = 'new', name = 'b.txt', entry_type = 'file', id = file[FIELD_ID] },
}
local actions = mutator.create_actions_from_diffs({
[bufnr] = diffs,
})
assert.are.same({
{
type = 'copy',
entry_type = 'file',
src_url = 'oil-test:///foo/a.txt',
dest_url = 'oil-test:///foo/b.txt',
},
}, actions)
end)
it('constructs MOVE actions', function()
local file = test_adapter.test_set('/foo/a.txt', 'file')
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
local diffs = {
{ type = 'delete', name = 'a.txt', id = file[FIELD_ID] },
{ type = 'new', name = 'b.txt', entry_type = 'file', id = file[FIELD_ID] },
}
local actions = mutator.create_actions_from_diffs({
[bufnr] = diffs,
})
assert.are.same({
{
type = 'move',
entry_type = 'file',
src_url = 'oil-test:///foo/a.txt',
dest_url = 'oil-test:///foo/b.txt',
},
}, actions)
end)
it('correctly orders MOVE + CREATE', function()
local file = test_adapter.test_set('/a.txt', 'file')
vim.cmd.edit({ args = { 'oil-test:///' } })
local bufnr = vim.api.nvim_get_current_buf()
local diffs = {
{ type = 'delete', name = 'a.txt', id = file[FIELD_ID] },
{ type = 'new', name = 'b.txt', entry_type = 'file', id = file[FIELD_ID] },
{ type = 'new', name = 'a.txt', entry_type = 'file' },
}
local actions = mutator.create_actions_from_diffs({
[bufnr] = diffs,
})
assert.are.same({
{
type = 'move',
entry_type = 'file',
src_url = 'oil-test:///a.txt',
dest_url = 'oil-test:///b.txt',
},
{
type = 'create',
entry_type = 'file',
url = 'oil-test:///a.txt',
},
}, actions)
end)
it('resolves MOVE loops', function()
local afile = test_adapter.test_set('/a.txt', 'file')
local bfile = test_adapter.test_set('/b.txt', 'file')
vim.cmd.edit({ args = { 'oil-test:///' } })
local bufnr = vim.api.nvim_get_current_buf()
local diffs = {
{ type = 'delete', name = 'a.txt', id = afile[FIELD_ID] },
{ type = 'new', name = 'b.txt', entry_type = 'file', id = afile[FIELD_ID] },
{ type = 'delete', name = 'b.txt', id = bfile[FIELD_ID] },
{ type = 'new', name = 'a.txt', entry_type = 'file', id = bfile[FIELD_ID] },
}
math.randomseed(2983982)
local actions = mutator.create_actions_from_diffs({
[bufnr] = diffs,
})
local tmp_url = 'oil-test:///a.txt__oil_tmp_510852'
assert.are.same({
{
type = 'move',
entry_type = 'file',
src_url = 'oil-test:///a.txt',
dest_url = tmp_url,
},
{
type = 'move',
entry_type = 'file',
src_url = 'oil-test:///b.txt',
dest_url = 'oil-test:///a.txt',
},
{
type = 'move',
entry_type = 'file',
src_url = tmp_url,
dest_url = 'oil-test:///b.txt',
},
}, actions)
end)
end)
describe('order actions', function()
it('Creates files inside dir before move', function()
local move = {
type = 'move',
src_url = 'oil-test:///a',
dest_url = 'oil-test:///b',
entry_type = 'directory',
}
local create = { type = 'create', url = 'oil-test:///a/hi.txt', entry_type = 'file' }
local actions = { move, create }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ create, move }, ordered_actions)
end)
it('Moves file out of parent before deleting parent', function()
local move = {
type = 'move',
src_url = 'oil-test:///a/b.txt',
dest_url = 'oil-test:///b.txt',
entry_type = 'file',
}
local delete = { type = 'delete', url = 'oil-test:///a', entry_type = 'directory' }
local actions = { delete, move }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ move, delete }, ordered_actions)
end)
it('Handles parent child move ordering', function()
local move1 = {
type = 'move',
src_url = 'oil-test:///a/b',
dest_url = 'oil-test:///b',
entry_type = 'directory',
}
local move2 = {
type = 'move',
src_url = 'oil-test:///a',
dest_url = 'oil-test:///b/a',
entry_type = 'directory',
}
local actions = { move2, move1 }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ move1, move2 }, ordered_actions)
end)
it('Handles a delete inside a moved folder', function()
local del = {
type = 'delete',
url = 'oil-test:///a/b.txt',
entry_type = 'file',
}
local move = {
type = 'move',
src_url = 'oil-test:///a',
dest_url = 'oil-test:///b',
entry_type = 'directory',
}
local actions = { move, del }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ del, move }, ordered_actions)
end)
it('Detects move directory loops', function()
local move = {
type = 'move',
src_url = 'oil-test:///a',
dest_url = 'oil-test:///a/b',
entry_type = 'directory',
}
assert.has_error(function()
mutator.enforce_action_order({ move })
end)
end)
it('Detects copy directory loops', function()
local move = {
type = 'copy',
src_url = 'oil-test:///a',
dest_url = 'oil-test:///a/b',
entry_type = 'directory',
}
assert.has_error(function()
mutator.enforce_action_order({ move })
end)
end)
it('Detects nested copy directory loops', function()
local move = {
type = 'copy',
src_url = 'oil-test:///a',
dest_url = 'oil-test:///a/b/a',
entry_type = 'directory',
}
assert.has_error(function()
mutator.enforce_action_order({ move })
end)
end)
describe('change', function()
it('applies CHANGE after CREATE', function()
local create = { type = 'create', url = 'oil-test:///a/hi.txt', entry_type = 'file' }
local change = {
type = 'change',
url = 'oil-test:///a/hi.txt',
entry_type = 'file',
column = 'TEST',
value = 'TEST',
}
local actions = { change, create }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ create, change }, ordered_actions)
end)
it('applies CHANGE after COPY src', function()
local copy = {
type = 'copy',
src_url = 'oil-test:///a/hi.txt',
dest_url = 'oil-test:///b.txt',
entry_type = 'file',
}
local change = {
type = 'change',
url = 'oil-test:///a/hi.txt',
entry_type = 'file',
column = 'TEST',
value = 'TEST',
}
local actions = { change, copy }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ copy, change }, ordered_actions)
end)
it('applies CHANGE after COPY dest', function()
local copy = {
type = 'copy',
src_url = 'oil-test:///b.txt',
dest_url = 'oil-test:///a/hi.txt',
entry_type = 'file',
}
local change = {
type = 'change',
url = 'oil-test:///a/hi.txt',
entry_type = 'file',
column = 'TEST',
value = 'TEST',
}
local actions = { change, copy }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ copy, change }, ordered_actions)
end)
it('applies CHANGE after MOVE dest', function()
local move = {
type = 'move',
src_url = 'oil-test:///b.txt',
dest_url = 'oil-test:///a/hi.txt',
entry_type = 'file',
}
local change = {
type = 'change',
url = 'oil-test:///a/hi.txt',
entry_type = 'file',
column = 'TEST',
value = 'TEST',
}
local actions = { change, move }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ move, change }, ordered_actions)
end)
end)
end)
describe('perform actions', function()
it('creates new entries', function()
local actions = {
{ type = 'create', url = 'oil-test:///a.txt', entry_type = 'file' },
}
test_util.await(mutator.process_actions, 2, actions)
local files = cache.list_url('oil-test:///')
assert.are.same({
['a.txt'] = {
[FIELD_ID] = 1,
[FIELD_TYPE] = 'file',
[FIELD_NAME] = 'a.txt',
},
}, files)
end)
it('deletes entries', function()
local file = test_adapter.test_set('/a.txt', 'file')
local actions = {
{ type = 'delete', url = 'oil-test:///a.txt', entry_type = 'file' },
}
test_util.await(mutator.process_actions, 2, actions)
local files = cache.list_url('oil-test:///')
assert.are.same({}, files)
assert.is_nil(cache.get_entry_by_id(file[FIELD_ID]))
assert.has_error(function()
cache.get_parent_url(file[FIELD_ID])
end)
end)
it('moves entries', function()
local file = test_adapter.test_set('/a.txt', 'file')
local actions = {
{
type = 'move',
src_url = 'oil-test:///a.txt',
dest_url = 'oil-test:///b.txt',
entry_type = 'file',
},
}
test_util.await(mutator.process_actions, 2, actions)
local files = cache.list_url('oil-test:///')
local new_entry = {
[FIELD_ID] = file[FIELD_ID],
[FIELD_TYPE] = 'file',
[FIELD_NAME] = 'b.txt',
}
assert.are.same({
['b.txt'] = new_entry,
}, files)
assert.are.same(new_entry, cache.get_entry_by_id(file[FIELD_ID]))
assert.equals('oil-test:///', cache.get_parent_url(file[FIELD_ID]))
end)
it('copies entries', function()
local file = test_adapter.test_set('/a.txt', 'file')
local actions = {
{
type = 'copy',
src_url = 'oil-test:///a.txt',
dest_url = 'oil-test:///b.txt',
entry_type = 'file',
},
}
test_util.await(mutator.process_actions, 2, actions)
local files = cache.list_url('oil-test:///')
local new_entry = {
[FIELD_ID] = file[FIELD_ID] + 1,
[FIELD_TYPE] = 'file',
[FIELD_NAME] = 'b.txt',
}
assert.are.same({
['a.txt'] = file,
['b.txt'] = new_entry,
}, files)
end)
end)
end)

249
spec/parser_spec.lua Normal file
View file

@ -0,0 +1,249 @@
local constants = require('oil.constants')
local parser = require('oil.mutator.parser')
local test_adapter = require('oil.adapters.test')
local test_util = require('spec.test_util')
local util = require('oil.util')
local view = require('oil.view')
local FIELD_ID = constants.FIELD_ID
local FIELD_META = constants.FIELD_META
local function set_lines(bufnr, lines)
vim.bo[bufnr].modifiable = true
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
end
describe('parser', function()
after_each(function()
test_util.reset_editor()
end)
it('detects new files', function()
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
'a.txt',
})
local diffs = parser.parse(bufnr)
assert.are.same({ { entry_type = 'file', name = 'a.txt', type = 'new' } }, diffs)
end)
it('detects new directories', function()
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
'foo/',
})
local diffs = parser.parse(bufnr)
assert.are.same({ { entry_type = 'directory', name = 'foo', type = 'new' } }, diffs)
end)
it('detects new links', function()
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
'a.txt -> b.txt',
})
local diffs = parser.parse(bufnr)
assert.are.same(
{ { entry_type = 'link', name = 'a.txt', type = 'new', link = 'b.txt' } },
diffs
)
end)
it('detects deleted files', function()
local file = test_adapter.test_set('/foo/a.txt', 'file')
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {})
local diffs = parser.parse(bufnr)
assert.are.same({
{ name = 'a.txt', type = 'delete', id = file[FIELD_ID] },
}, diffs)
end)
it('detects deleted directories', function()
local dir = test_adapter.test_set('/foo/bar', 'directory')
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {})
local diffs = parser.parse(bufnr)
assert.are.same({
{ name = 'bar', type = 'delete', id = dir[FIELD_ID] },
}, diffs)
end)
it('detects deleted links', function()
local file = test_adapter.test_set('/foo/a.txt', 'link')
file[FIELD_META] = { link = 'b.txt' }
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {})
local diffs = parser.parse(bufnr)
assert.are.same({
{ name = 'a.txt', type = 'delete', id = file[FIELD_ID] },
}, diffs)
end)
it('ignores empty lines', function()
local file = test_adapter.test_set('/foo/a.txt', 'file')
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
local cols = view.format_entry_cols(file, {}, {}, test_adapter, false)
local lines = util.render_table({ cols }, {})
table.insert(lines, '')
table.insert(lines, ' ')
set_lines(bufnr, lines)
local diffs = parser.parse(bufnr)
assert.are.same({}, diffs)
end)
it('errors on missing filename', function()
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
'/008',
})
local _, errors = parser.parse(bufnr)
assert.are_same({
{
message = 'Malformed ID at start of line',
lnum = 0,
end_lnum = 1,
col = 0,
},
}, errors)
end)
it('errors on empty dirname', function()
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
'/008 /',
})
local _, errors = parser.parse(bufnr)
assert.are.same({
{
message = 'No filename found',
lnum = 0,
end_lnum = 1,
col = 0,
},
}, errors)
end)
it('errors on duplicate names', function()
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
'foo',
'foo/',
})
local _, errors = parser.parse(bufnr)
assert.are.same({
{
message = 'Duplicate filename',
lnum = 1,
end_lnum = 2,
col = 0,
},
}, errors)
end)
it('errors on duplicate names for existing files', function()
local file = test_adapter.test_set('/foo/a.txt', 'file')
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
'a.txt',
string.format('/%d a.txt', file[FIELD_ID]),
})
local _, errors = parser.parse(bufnr)
assert.are.same({
{
message = 'Duplicate filename',
lnum = 1,
end_lnum = 2,
col = 0,
},
}, errors)
end)
it('ignores new dirs with empty name', function()
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
'/',
})
local diffs = parser.parse(bufnr)
assert.are.same({}, diffs)
end)
it('parses a rename as a delete + new', function()
local file = test_adapter.test_set('/foo/a.txt', 'file')
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
string.format('/%d b.txt', file[FIELD_ID]),
})
local diffs = parser.parse(bufnr)
assert.are.same({
{ type = 'new', id = file[FIELD_ID], name = 'b.txt', entry_type = 'file' },
{ type = 'delete', id = file[FIELD_ID], name = 'a.txt' },
}, diffs)
end)
it('detects a new trailing slash as a delete + create', function()
local file = test_adapter.test_set('/foo', 'file')
vim.cmd.edit({ args = { 'oil-test:///' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
string.format('/%d foo/', file[FIELD_ID]),
})
local diffs = parser.parse(bufnr)
assert.are.same({
{ type = 'new', name = 'foo', entry_type = 'directory' },
{ type = 'delete', id = file[FIELD_ID], name = 'foo' },
}, diffs)
end)
it('detects renamed files that conflict', function()
local afile = test_adapter.test_set('/foo/a.txt', 'file')
local bfile = test_adapter.test_set('/foo/b.txt', 'file')
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
string.format('/%d a.txt', bfile[FIELD_ID]),
string.format('/%d b.txt', afile[FIELD_ID]),
})
local diffs = parser.parse(bufnr)
local first_two = { diffs[1], diffs[2] }
local last_two = { diffs[3], diffs[4] }
table.sort(first_two, function(a, b)
return a.id < b.id
end)
table.sort(last_two, function(a, b)
return a.id < b.id
end)
assert.are.same({
{ name = 'b.txt', type = 'new', id = afile[FIELD_ID], entry_type = 'file' },
{ name = 'a.txt', type = 'new', id = bfile[FIELD_ID], entry_type = 'file' },
}, first_two)
assert.are.same({
{ name = 'a.txt', type = 'delete', id = afile[FIELD_ID] },
{ name = 'b.txt', type = 'delete', id = bfile[FIELD_ID] },
}, last_two)
end)
it('views link targets with trailing slashes as the same', function()
local file = test_adapter.test_set('/foo/mydir', 'link')
file[FIELD_META] = { link = 'dir/' }
vim.cmd.edit({ args = { 'oil-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
string.format('/%d mydir/ -> dir/', file[FIELD_ID]),
})
local diffs = parser.parse(bufnr)
assert.are.same({}, diffs)
end)
end)

View file

@ -1,13 +1,13 @@
local pathutil = require("oil.pathutil") local pathutil = require('oil.pathutil')
describe("pathutil", function() describe('pathutil', function()
it("calculates parent path", function() it('calculates parent path', function()
local cases = { local cases = {
{ "/foo/bar", "/foo/" }, { '/foo/bar', '/foo/' },
{ "/foo/bar/", "/foo/" }, { '/foo/bar/', '/foo/' },
{ "/", "/" }, { '/', '/' },
{ "", "" }, { '', '' },
{ "foo/bar/", "foo/" }, { 'foo/bar/', 'foo/' },
{ "foo", "" }, { 'foo', '' },
} }
for _, case in ipairs(cases) do for _, case in ipairs(cases) do
local input, expected = unpack(case) local input, expected = unpack(case)
@ -16,12 +16,12 @@ describe("pathutil", function()
end end
end) end)
it("calculates basename", function() it('calculates basename', function()
local cases = { local cases = {
{ "/foo/bar", "bar" }, { '/foo/bar', 'bar' },
{ "/foo/bar/", "bar" }, { '/foo/bar/', 'bar' },
{ "/", nil }, { '/', nil },
{ "", nil }, { '', nil },
} }
for _, case in ipairs(cases) do for _, case in ipairs(cases) do
local input, expected = unpack(case) local input, expected = unpack(case)

View file

@ -1,41 +1,40 @@
require("plenary.async").tests.add_to_env() local TmpDir = require('spec.tmpdir')
local TmpDir = require("tests.tmpdir") local oil = require('oil')
local oil = require("oil") local test_util = require('spec.test_util')
local test_util = require("tests.test_util") local util = require('oil.util')
local util = require("oil.util")
a.describe("oil preview", function() describe('oil preview', function()
local tmpdir local tmpdir
a.before_each(function() before_each(function()
tmpdir = TmpDir.new() tmpdir = TmpDir.new()
end) end)
a.after_each(function() after_each(function()
if tmpdir then if tmpdir then
tmpdir:dispose() tmpdir:dispose()
end end
test_util.reset_editor() test_util.reset_editor()
end) end)
a.it("opens preview window", function() it('opens preview window', function()
tmpdir:create({ "a.txt" }) tmpdir:create({ 'a.txt' })
test_util.oil_open(tmpdir.path) test_util.oil_open(tmpdir.path)
a.wrap(oil.open_preview, 2)() test_util.await(oil.open_preview, 2)
local preview_win = util.get_preview_win() local preview_win = util.get_preview_win()
assert.not_nil(preview_win) assert.not_nil(preview_win)
assert(preview_win) assert(preview_win)
local bufnr = vim.api.nvim_win_get_buf(preview_win) local bufnr = vim.api.nvim_win_get_buf(preview_win)
local preview_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local preview_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
assert.are.same({ "a.txt" }, preview_lines) assert.are.same({ 'a.txt' }, preview_lines)
end) end)
a.it("opens preview window when open(preview={})", function() it('opens preview window when open(preview={})', function()
tmpdir:create({ "a.txt" }) tmpdir:create({ 'a.txt' })
test_util.oil_open(tmpdir.path, { preview = {} }) test_util.oil_open(tmpdir.path, { preview = {} })
local preview_win = util.get_preview_win() local preview_win = util.get_preview_win()
assert.not_nil(preview_win) assert.not_nil(preview_win)
assert(preview_win) assert(preview_win)
local bufnr = vim.api.nvim_win_get_buf(preview_win) local bufnr = vim.api.nvim_win_get_buf(preview_win)
local preview_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local preview_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
assert.are.same({ "a.txt" }, preview_lines) assert.are.same({ 'a.txt' }, preview_lines)
end) end)
end) end)

137
spec/regression_spec.lua Normal file
View file

@ -0,0 +1,137 @@
local TmpDir = require('spec.tmpdir')
local actions = require('oil.actions')
local oil = require('oil')
local test_util = require('spec.test_util')
local view = require('oil.view')
describe('regression tests', function()
local tmpdir
before_each(function()
tmpdir = TmpDir.new()
end)
after_each(function()
if tmpdir then
tmpdir:dispose()
tmpdir = nil
end
test_util.reset_editor()
end)
it('can edit dirs that will be renamed to an existing buffer', function()
vim.cmd.edit({ args = { 'README.md' } })
vim.cmd.vsplit()
vim.cmd.edit({ args = { '%:p:h' } })
assert.equals('oil', vim.bo.filetype)
vim.cmd.wincmd({ args = { 'p' } })
assert.equals('markdown', vim.bo.filetype)
vim.cmd.edit({ args = { '%:p:h' } })
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
assert.equals('oil', vim.bo.filetype)
end)
it('places the cursor on correct entry when opening on file', function()
vim.cmd.edit({ args = { '.' } })
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
local entry = oil.get_cursor_entry()
assert.not_nil(entry)
assert.not_equals('README.md', entry and entry.name)
vim.cmd.edit({ args = { 'README.md' } })
view.delete_hidden_buffers()
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
entry = oil.get_cursor_entry()
assert.equals('README.md', entry and entry.name)
end)
it("doesn't close floating windows oil didn't open itself", function()
local winid = vim.api.nvim_open_win(vim.fn.bufadd('README.md'), true, {
relative = 'editor',
row = 1,
col = 1,
width = 100,
height = 100,
})
oil.open()
vim.wait(10)
oil.close()
vim.wait(10)
assert.equals(winid, vim.api.nvim_get_current_win())
end)
it("doesn't close splits on oil.close", function()
vim.cmd.edit({ args = { 'README.md' } })
vim.cmd.vsplit()
local winid = vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_get_current_buf()
oil.open()
vim.wait(10)
oil.close()
vim.wait(10)
assert.equals(2, #vim.api.nvim_tabpage_list_wins(0))
assert.equals(winid, vim.api.nvim_get_current_win())
assert.equals(bufnr, vim.api.nvim_get_current_buf())
end)
it('Returns to empty buffer on close', function()
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
oil.close()
assert.not_equals('oil', vim.bo.filetype)
assert.equals('', vim.api.nvim_buf_get_name(0))
end)
it('All buffers set nomodified after save', function()
tmpdir:create({ 'a.txt' })
vim.cmd.edit({ args = { 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') } })
local first_dir = vim.api.nvim_get_current_buf()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
test_util.feedkeys({ 'dd', 'itest/<esc>', '<CR>' }, 10)
vim.wait(1000, function()
return vim.bo.modifiable
end, 10)
test_util.feedkeys({ 'p' }, 10)
oil.save({ confirm = false })
vim.wait(1000, function()
return vim.bo.modifiable
end, 10)
tmpdir:assert_fs({
['test/a.txt'] = 'a.txt',
})
assert.falsy(vim.bo[first_dir].modified)
end)
it("refreshing buffer doesn't lose track of it", function()
vim.cmd.edit({ args = { '.' } })
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
local bufnr = vim.api.nvim_get_current_buf()
vim.cmd.edit({ bang = true })
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
assert.are.same({ bufnr }, require('oil.view').get_all_buffers())
end)
it('can copy a file multiple times', function()
test_util.actions.open({ tmpdir.path })
vim.api.nvim_feedkeys('ifoo.txt', 'x', true)
test_util.actions.save()
vim.api.nvim_feedkeys('yyp$ciWbar.txt', 'x', true)
vim.api.nvim_feedkeys('yyp$ciWbaz.txt', 'x', true)
test_util.actions.save()
assert.are.same({ 'bar.txt', 'baz.txt', 'foo.txt' }, test_util.parse_entries(0))
tmpdir:assert_fs({
['foo.txt'] = '',
['bar.txt'] = '',
['baz.txt'] = '',
})
end)
it('can open files from floating window', function()
tmpdir:create({ 'a.txt' })
oil.open_float(tmpdir.path)
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
actions.select.callback()
vim.wait(1000, function()
return vim.fn.expand('%:t') == 'a.txt'
end, 10)
assert.equals('a.txt', vim.fn.expand('%:t'))
end)
end)

View file

@ -1,90 +1,85 @@
require("plenary.async").tests.add_to_env() local oil = require('oil')
local oil = require("oil") local test_util = require('spec.test_util')
local test_util = require("tests.test_util")
a.describe("oil select", function() describe('oil select', function()
after_each(function() after_each(function()
test_util.reset_editor() test_util.reset_editor()
end) end)
a.it("opens file under cursor", function() it('opens file under cursor', function()
test_util.oil_open() test_util.oil_open()
-- Go to the bottom, so the cursor is not on a directory vim.cmd.normal({ args = { 'G' } })
vim.cmd.normal({ args = { "G" } }) test_util.await(oil.select, 2)
a.wrap(oil.select, 2)()
assert.equals(1, #vim.api.nvim_tabpage_list_wins(0)) assert.equals(1, #vim.api.nvim_tabpage_list_wins(0))
assert.not_equals("oil", vim.bo.filetype) assert.not_equals('oil', vim.bo.filetype)
end) end)
a.it("opens file in new tab", function() it('opens file in new tab', function()
test_util.oil_open() test_util.oil_open()
local tabpage = vim.api.nvim_get_current_tabpage() local tabpage = vim.api.nvim_get_current_tabpage()
a.wrap(oil.select, 2)({ tab = true }) test_util.await(oil.select, 2, { tab = true })
assert.equals(2, #vim.api.nvim_list_tabpages()) assert.equals(2, #vim.api.nvim_list_tabpages())
assert.equals(1, #vim.api.nvim_tabpage_list_wins(0)) assert.equals(1, #vim.api.nvim_tabpage_list_wins(0))
assert.not_equals(tabpage, vim.api.nvim_get_current_tabpage()) assert.not_equals(tabpage, vim.api.nvim_get_current_tabpage())
end) end)
a.it("opens file in new split", function() it('opens file in new split', function()
test_util.oil_open() test_util.oil_open()
local winid = vim.api.nvim_get_current_win() local winid = vim.api.nvim_get_current_win()
a.wrap(oil.select, 2)({ vertical = true }) test_util.await(oil.select, 2, { vertical = true })
assert.equals(1, #vim.api.nvim_list_tabpages()) assert.equals(1, #vim.api.nvim_list_tabpages())
assert.equals(2, #vim.api.nvim_tabpage_list_wins(0)) assert.equals(2, #vim.api.nvim_tabpage_list_wins(0))
assert.not_equals(winid, vim.api.nvim_get_current_win()) assert.not_equals(winid, vim.api.nvim_get_current_win())
end) end)
a.it("opens multiple files in new tabs", function() it('opens multiple files in new tabs', function()
test_util.oil_open() test_util.oil_open()
vim.api.nvim_feedkeys("Vj", "x", true) vim.api.nvim_feedkeys('Vj', 'x', true)
local tabpage = vim.api.nvim_get_current_tabpage() local tabpage = vim.api.nvim_get_current_tabpage()
a.wrap(oil.select, 2)({ tab = true }) test_util.await(oil.select, 2, { tab = true })
assert.equals(3, #vim.api.nvim_list_tabpages()) assert.equals(3, #vim.api.nvim_list_tabpages())
assert.equals(1, #vim.api.nvim_tabpage_list_wins(0)) assert.equals(1, #vim.api.nvim_tabpage_list_wins(0))
assert.not_equals(tabpage, vim.api.nvim_get_current_tabpage()) assert.not_equals(tabpage, vim.api.nvim_get_current_tabpage())
end) end)
a.it("opens multiple files in new splits", function() it('opens multiple files in new splits', function()
test_util.oil_open() test_util.oil_open()
vim.api.nvim_feedkeys("Vj", "x", true) vim.api.nvim_feedkeys('Vj', 'x', true)
local winid = vim.api.nvim_get_current_win() local winid = vim.api.nvim_get_current_win()
a.wrap(oil.select, 2)({ vertical = true }) test_util.await(oil.select, 2, { vertical = true })
assert.equals(1, #vim.api.nvim_list_tabpages()) assert.equals(1, #vim.api.nvim_list_tabpages())
assert.equals(3, #vim.api.nvim_tabpage_list_wins(0)) assert.equals(3, #vim.api.nvim_tabpage_list_wins(0))
assert.not_equals(winid, vim.api.nvim_get_current_win()) assert.not_equals(winid, vim.api.nvim_get_current_win())
end) end)
a.describe("close after open", function() describe('close after open', function()
a.it("same window", function() it('same window', function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { 'foo' } })
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
test_util.oil_open() test_util.oil_open()
-- Go to the bottom, so the cursor is not on a directory vim.cmd.normal({ args = { 'G' } })
vim.cmd.normal({ args = { "G" } }) test_util.await(oil.select, 2, { close = true })
a.wrap(oil.select, 2)({ close = true })
assert.equals(1, #vim.api.nvim_tabpage_list_wins(0)) assert.equals(1, #vim.api.nvim_tabpage_list_wins(0))
-- This one we actually don't expect the buffer to be the same as the initial buffer, because
-- we opened a file
assert.not_equals(bufnr, vim.api.nvim_get_current_buf()) assert.not_equals(bufnr, vim.api.nvim_get_current_buf())
assert.not_equals("oil", vim.bo.filetype) assert.not_equals('oil', vim.bo.filetype)
end) end)
a.it("split", function() it('split', function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { 'foo' } })
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local winid = vim.api.nvim_get_current_win() local winid = vim.api.nvim_get_current_win()
test_util.oil_open() test_util.oil_open()
a.wrap(oil.select, 2)({ vertical = true, close = true }) test_util.await(oil.select, 2, { vertical = true, close = true })
assert.equals(2, #vim.api.nvim_tabpage_list_wins(0)) assert.equals(2, #vim.api.nvim_tabpage_list_wins(0))
assert.equals(bufnr, vim.api.nvim_win_get_buf(winid)) assert.equals(bufnr, vim.api.nvim_win_get_buf(winid))
end) end)
a.it("tab", function() it('tab', function()
vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { 'foo' } })
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local tabpage = vim.api.nvim_get_current_tabpage() local tabpage = vim.api.nvim_get_current_tabpage()
test_util.oil_open() test_util.oil_open()
a.wrap(oil.select, 2)({ tab = true, close = true }) test_util.await(oil.select, 2, { tab = true, close = true })
assert.equals(1, #vim.api.nvim_tabpage_list_wins(0)) assert.equals(1, #vim.api.nvim_tabpage_list_wins(0))
assert.equals(2, #vim.api.nvim_list_tabpages()) assert.equals(2, #vim.api.nvim_list_tabpages())
vim.api.nvim_set_current_tabpage(tabpage) vim.api.nvim_set_current_tabpage(tabpage)

View file

@ -1,24 +1,19 @@
require("plenary.async").tests.add_to_env() local cache = require('oil.cache')
local cache = require("oil.cache") local test_adapter = require('oil.adapters.test')
local test_adapter = require("oil.adapters.test") local util = require('oil.util')
local util = require("oil.util")
local M = {} local M = {}
M.reset_editor = function() M.reset_editor = function()
require("oil").setup({ require('oil').setup({
columms = {}, columms = {},
adapters = { adapters = {
["oil-test://"] = "test", ['oil-test://'] = 'test',
}, },
prompt_save_on_select_new_entry = false, prompt_save_on_select_new_entry = false,
}) })
vim.cmd.tabonly({ mods = { silent = true } }) vim.cmd.tabonly({ mods = { silent = true } })
for i, winid in ipairs(vim.api.nvim_tabpage_list_wins(0)) do vim.cmd.new()
if i > 1 then vim.cmd.only()
vim.api.nvim_win_close(winid, true)
end
end
vim.api.nvim_win_set_buf(0, vim.api.nvim_create_buf(false, true))
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
vim.api.nvim_buf_delete(bufnr, { force = true }) vim.api.nvim_buf_delete(bufnr, { force = true })
end end
@ -34,49 +29,87 @@ local function throwiferr(err, ...)
end end
end end
M.oil_open = function(...)
a.wrap(require("oil").open, 3)(...)
end
M.await = function(fn, nargs, ...) M.await = function(fn, nargs, ...)
return throwiferr(a.wrap(fn, nargs)(...)) local done = false
local results
local n_results = 0
local args = { ... }
args[nargs] = function(...)
results = { ... }
n_results = select('#', ...)
done = true
end
fn(unpack(args, 1, nargs))
vim.wait(10000, function()
return done
end, 10)
if not done then
error('M.await timed out')
end
return unpack(results, 1, n_results)
end end
M.wait_for_autocmd = a.wrap(function(autocmd, cb) M.await_throwiferr = function(fn, nargs, ...)
return throwiferr(M.await(fn, nargs, ...))
end
M.oil_open = function(...)
M.await(require('oil').open, 3, ...)
end
M.wait_for_autocmd = function(autocmd)
local triggered = false
local opts = { local opts = {
pattern = "*", pattern = '*',
nested = true, nested = true,
once = true, once = true,
} }
if type(autocmd) == "table" then if type(autocmd) == 'table' then
opts = vim.tbl_extend("force", opts, autocmd) opts = vim.tbl_extend('force', opts, autocmd)
autocmd = autocmd[1] autocmd = autocmd[1]
opts[1] = nil opts[1] = nil
end end
opts.callback = vim.schedule_wrap(cb) opts.callback = vim.schedule_wrap(function()
triggered = true
end)
vim.api.nvim_create_autocmd(autocmd, opts) vim.api.nvim_create_autocmd(autocmd, opts)
end, 2) vim.wait(10000, function()
return triggered
end, 10)
if not triggered then
error('wait_for_autocmd timed out waiting for ' .. tostring(autocmd))
end
end
M.wait_oil_ready = a.wrap(function(cb) M.wait_oil_ready = function()
util.run_after_load(0, vim.schedule_wrap(cb)) local ready = false
end, 1) util.run_after_load(
0,
vim.schedule_wrap(function()
ready = true
end)
)
vim.wait(10000, function()
return ready
end, 10)
if not ready then
error('wait_oil_ready timed out')
end
end
---@param actions string[] ---@param actions string[]
---@param timestep integer ---@param timestep integer
M.feedkeys = function(actions, timestep) M.feedkeys = function(actions, timestep)
timestep = timestep or 10 timestep = timestep or 10
a.util.sleep(timestep) vim.wait(timestep)
for _, action in ipairs(actions) do for _, action in ipairs(actions) do
a.util.sleep(timestep) vim.wait(timestep)
local escaped = vim.api.nvim_replace_termcodes(action, true, false, true) local escaped = vim.api.nvim_replace_termcodes(action, true, false, true)
vim.api.nvim_feedkeys(escaped, "m", true) vim.api.nvim_feedkeys(escaped, 'm', true)
end end
a.util.sleep(timestep) vim.wait(timestep)
-- process pending keys until the queue is empty. vim.api.nvim_feedkeys('', 'x', true)
-- Note that this will exit insert mode. vim.wait(timestep)
vim.api.nvim_feedkeys("", "x", true)
a.util.sleep(timestep)
end end
M.actions = { M.actions = {
@ -85,41 +118,40 @@ M.actions = {
open = function(args) open = function(args)
vim.schedule(function() vim.schedule(function()
vim.cmd.Oil({ args = args }) vim.cmd.Oil({ args = args })
-- If this buffer was already open, manually dispatch the autocmd to finish the wait
if vim.b.oil_ready then if vim.b.oil_ready then
vim.api.nvim_exec_autocmds("User", { vim.api.nvim_exec_autocmds('User', {
pattern = "OilEnter", pattern = 'OilEnter',
modeline = false, modeline = false,
data = { buf = vim.api.nvim_get_current_buf() }, data = { buf = vim.api.nvim_get_current_buf() },
}) })
end end
end) end)
M.wait_for_autocmd({ "User", pattern = "OilEnter" }) M.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
end, end,
---Save all changes and wait for operation to complete ---Save all changes and wait for operation to complete
save = function() save = function()
vim.schedule_wrap(require("oil").save)({ confirm = false }) vim.schedule_wrap(require('oil').save)({ confirm = false })
M.wait_for_autocmd({ "User", pattern = "OilMutationComplete" }) M.wait_for_autocmd({ 'User', pattern = 'OilMutationComplete' })
end, end,
---@param bufnr? integer ---@param bufnr? integer
reload = function(bufnr) reload = function(bufnr)
M.await(require("oil.view").render_buffer_async, 3, bufnr or 0) M.await(require('oil.view').render_buffer_async, 3, bufnr or 0)
end, end,
---Move cursor to a file or directory in an oil buffer ---Move cursor to a file or directory in an oil buffer
---@param filename string ---@param filename string
focus = function(filename) focus = function(filename)
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, true) local lines = vim.api.nvim_buf_get_lines(0, 0, -1, true)
local search = " " .. filename .. "$" local search = ' ' .. filename .. '$'
for i, line in ipairs(lines) do for i, line in ipairs(lines) do
if line:match(search) then if line:match(search) then
vim.api.nvim_win_set_cursor(0, { i, 0 }) vim.api.nvim_win_set_cursor(0, { i, 0 })
return return
end end
end end
error("Could not find file " .. filename) error('Could not find file ' .. filename)
end, end,
} }
@ -133,7 +165,7 @@ M.parse_entries = function(bufnr)
end end
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
return vim.tbl_map(function(line) return vim.tbl_map(function(line)
return line:match("^/%d+ +(.+)$") return line:match('^/%d+ +(.+)$')
end, lines) end, lines)
end end

View file

@ -1,25 +1,18 @@
local fs = require("oil.fs") local fs = require('oil.fs')
local test_util = require("tests.test_util") local test_util = require('spec.test_util')
local await = test_util.await
---@param path string ---@param path string
---@param cb fun(err: nil|string) local function touch(path)
local function touch(path, cb) local fd, open_err = vim.loop.fs_open(path, 'w', 420) -- 0644
vim.loop.fs_open(path, "w", 420, function(err, fd) -- 0644 if not fd then
if err then error(open_err)
cb(err) end
else local shortpath = path:gsub('^[^' .. fs.sep .. ']*' .. fs.sep, '')
local shortpath = path:gsub("^[^" .. fs.sep .. "]*" .. fs.sep, "") local _, write_err = vim.loop.fs_write(fd, shortpath)
vim.loop.fs_write(fd, shortpath, nil, function(err2) if write_err then
if err2 then error(write_err)
cb(err2) end
else vim.loop.fs_close(fd)
vim.loop.fs_close(fd, cb)
end
end)
end
end)
end end
---@param filepath string ---@param filepath string
@ -28,11 +21,14 @@ local function exists(filepath)
local stat = vim.loop.fs_stat(filepath) local stat = vim.loop.fs_stat(filepath)
return stat ~= nil and stat.type ~= nil return stat ~= nil and stat.type ~= nil
end end
local TmpDir = {} local TmpDir = {}
TmpDir.new = function() TmpDir.new = function()
local path = await(vim.loop.fs_mkdtemp, 2, "oil_test_XXXXXXXXX") local path, err = vim.loop.fs_mkdtemp('oil_test_XXXXXXXXX')
a.util.scheduler() if not path then
error(err)
end
return setmetatable({ path = path }, { return setmetatable({ path = path }, {
__index = TmpDir, __index = TmpDir,
}) })
@ -46,31 +42,28 @@ function TmpDir:create(paths)
for i, piece in ipairs(pieces) do for i, piece in ipairs(pieces) do
partial_path = fs.join(partial_path, piece) partial_path = fs.join(partial_path, piece)
if i == #pieces and not vim.endswith(partial_path, fs.sep) then if i == #pieces and not vim.endswith(partial_path, fs.sep) then
await(touch, 2, partial_path) touch(partial_path)
elseif not exists(partial_path) then elseif not exists(partial_path) then
vim.loop.fs_mkdir(partial_path, 493) vim.loop.fs_mkdir(partial_path, 493)
end end
end end
end end
a.util.scheduler()
end end
---@param filepath string ---@param filepath string
---@return string? ---@return string?
local read_file = function(filepath) local read_file = function(filepath)
local fd = vim.loop.fs_open(filepath, "r", 420) local fd = vim.loop.fs_open(filepath, 'r', 420)
if not fd then if not fd then
return nil return nil
end end
local stat = vim.loop.fs_fstat(fd) local stat = vim.loop.fs_fstat(fd)
local content = vim.loop.fs_read(fd, stat.size) local content = vim.loop.fs_read(fd, stat.size)
vim.loop.fs_close(fd) vim.loop.fs_close(fd)
a.util.scheduler()
return content return content
end end
---@param dir string ---@param dir string
---@param cb fun(err: nil|string, entry: {type: oil.EntryType, name: string, root: string}
local function walk(dir) local function walk(dir)
local ret = {} local ret = {}
for name, type in vim.fs.dir(dir) do for name, type in vim.fs.dir(dir) do
@ -79,7 +72,7 @@ local function walk(dir)
type = type, type = type,
root = dir, root = dir,
}) })
if type == "directory" then if type == 'directory' then
vim.list_extend(ret, walk(fs.join(dir, name))) vim.list_extend(ret, walk(fs.join(dir, name)))
end end
end end
@ -90,10 +83,10 @@ end
local assert_fs = function(root, paths) local assert_fs = function(root, paths)
local unlisted_dirs = {} local unlisted_dirs = {}
for k in pairs(paths) do for k in pairs(paths) do
local pieces = vim.split(k, "/") local pieces = vim.split(k, '/')
local partial_path = "" local partial_path = ''
for i, piece in ipairs(pieces) do for i, piece in ipairs(pieces) do
partial_path = partial_path .. piece .. "/" partial_path = partial_path .. piece .. '/'
if i ~= #pieces then if i ~= #pieces then
unlisted_dirs[partial_path] = true unlisted_dirs[partial_path] = true
end end
@ -107,17 +100,16 @@ local assert_fs = function(root, paths)
for _, entry in ipairs(entries) do for _, entry in ipairs(entries) do
local fullpath = fs.join(entry.root, entry.name) local fullpath = fs.join(entry.root, entry.name)
local shortpath = fullpath:sub(root:len() + 2) local shortpath = fullpath:sub(root:len() + 2)
if entry.type == "directory" then if entry.type == 'directory' then
shortpath = shortpath .. "/" shortpath = shortpath .. '/'
end end
local expected_content = paths[shortpath] local expected_content = paths[shortpath]
paths[shortpath] = nil paths[shortpath] = nil
assert.truthy(expected_content, string.format("Unexpected entry '%s'", shortpath)) assert(expected_content, string.format("Unexpected entry '%s'", shortpath))
if entry.type == "file" then if entry.type == 'file' then
local data = read_file(fullpath) local data = read_file(fullpath)
assert.equals( assert(
expected_content, expected_content == data,
data,
string.format( string.format(
"File '%s' expected content '%s' received '%s'", "File '%s' expected content '%s' received '%s'",
shortpath, shortpath,
@ -129,11 +121,11 @@ local assert_fs = function(root, paths)
end end
for k, v in pairs(paths) do for k, v in pairs(paths) do
assert.falsy( assert(
k, not k,
string.format( string.format(
"Expected %s '%s', but it was not found", "Expected %s '%s', but it was not found",
v == true and "directory" or "file", v == true and 'directory' or 'file',
k k
) )
) )
@ -142,27 +134,23 @@ end
---@param paths table<string, string> ---@param paths table<string, string>
function TmpDir:assert_fs(paths) function TmpDir:assert_fs(paths)
a.util.scheduler()
assert_fs(self.path, paths) assert_fs(self.path, paths)
end end
function TmpDir:assert_exists(path) function TmpDir:assert_exists(path)
a.util.scheduler()
path = fs.join(self.path, path) path = fs.join(self.path, path)
local stat = vim.loop.fs_stat(path) local stat = vim.loop.fs_stat(path)
assert.truthy(stat, string.format("Expected path '%s' to exist", path)) assert(stat, string.format("Expected path '%s' to exist", path))
end end
function TmpDir:assert_not_exists(path) function TmpDir:assert_not_exists(path)
a.util.scheduler()
path = fs.join(self.path, path) path = fs.join(self.path, path)
local stat = vim.loop.fs_stat(path) local stat = vim.loop.fs_stat(path)
assert.falsy(stat, string.format("Expected path '%s' to not exist", path)) assert(not stat, string.format("Expected path '%s' to not exist", path))
end end
function TmpDir:dispose() function TmpDir:dispose()
await(fs.recursive_delete, 3, "directory", self.path) test_util.await_throwiferr(fs.recursive_delete, 3, 'directory', self.path)
a.util.scheduler()
end end
return TmpDir return TmpDir

149
spec/trash_spec.lua Normal file
View file

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

25
spec/url_spec.lua Normal file
View file

@ -0,0 +1,25 @@
local oil = require('oil')
local util = require('oil.util')
describe('url', function()
it('get_url_for_path', function()
local cases = {
{ '', 'oil://' .. util.addslash(vim.fn.getcwd()) },
{ 'term://~/oil.nvim//52953:/bin/sh', 'oil://' .. vim.loop.os_homedir() .. '/oil.nvim/' },
{ '/foo/bar.txt', 'oil:///foo/', 'bar.txt' },
{ 'oil:///foo/bar.txt', 'oil:///foo/', 'bar.txt' },
{ 'oil:///', 'oil:///' },
{ 'oil-ssh://user@hostname:8888//bar.txt', 'oil-ssh://user@hostname:8888//', 'bar.txt' },
{ 'oil-ssh://user@hostname:8888//', 'oil-ssh://user@hostname:8888//' },
}
for _, case in ipairs(cases) do
local input, expected, expected_basename = unpack(case)
local output, basename = oil.get_buffer_parent_url(input, true)
assert.equals(expected, output, string.format('Parent url for path "%s" failed', input))
assert.equals(
expected_basename,
basename,
string.format('Basename for path "%s" failed', input)
)
end
end)
end)

View file

@ -1,10 +1,10 @@
local util = require("oil.util") local util = require('oil.util')
describe("util", function() describe('util', function()
it("url_escape", function() it('url_escape', function()
local cases = { local cases = {
{ "foobar", "foobar" }, { 'foobar', 'foobar' },
{ "foo bar", "foo%20bar" }, { 'foo bar', 'foo%20bar' },
{ "/foo/bar", "%2Ffoo%2Fbar" }, { '/foo/bar', '%2Ffoo%2Fbar' },
} }
for _, case in ipairs(cases) do for _, case in ipairs(cases) do
local input, expected = unpack(case) local input, expected = unpack(case)
@ -13,12 +13,12 @@ describe("util", function()
end end
end) end)
it("url_unescape", function() it('url_unescape', function()
local cases = { local cases = {
{ "foobar", "foobar" }, { 'foobar', 'foobar' },
{ "foo%20bar", "foo bar" }, { 'foo%20bar', 'foo bar' },
{ "%2Ffoo%2Fbar", "/foo/bar" }, { '%2Ffoo%2Fbar', '/foo/bar' },
{ "foo%%bar", "foo%%bar" }, { 'foo%%bar', 'foo%%bar' },
} }
for _, case in ipairs(cases) do for _, case in ipairs(cases) do
local input, expected = unpack(case) local input, expected = unpack(case)

65
spec/win_options_spec.lua Normal file
View file

@ -0,0 +1,65 @@
local oil = require('oil')
local test_util = require('spec.test_util')
describe('window options', function()
after_each(function()
test_util.reset_editor()
end)
it('Restores window options on close', function()
vim.cmd.edit({ args = { 'README.md' } })
test_util.oil_open()
assert.equals('no', vim.o.signcolumn)
oil.close()
assert.equals('auto', vim.o.signcolumn)
end)
it('Restores window options on edit', function()
test_util.oil_open()
assert.equals('no', vim.o.signcolumn)
vim.cmd.edit({ args = { 'README.md' } })
assert.equals('auto', vim.o.signcolumn)
end)
it('Restores window options on split <filename>', function()
test_util.oil_open()
assert.equals('no', vim.o.signcolumn)
vim.cmd.split({ args = { 'README.md' } })
assert.equals('auto', vim.o.signcolumn)
end)
it('Restores window options on split', function()
test_util.oil_open()
assert.equals('no', vim.o.signcolumn)
vim.cmd.split()
vim.cmd.edit({ args = { 'README.md' } })
assert.equals('auto', vim.o.signcolumn)
end)
it('Restores window options on tabnew <filename>', function()
test_util.oil_open()
assert.equals('no', vim.o.signcolumn)
vim.cmd.tabnew({ args = { 'README.md' } })
assert.equals('auto', vim.o.signcolumn)
end)
it('Restores window options on tabnew', function()
test_util.oil_open()
assert.equals('no', vim.o.signcolumn)
vim.cmd.tabnew()
vim.cmd.edit({ args = { 'README.md' } })
assert.equals('auto', vim.o.signcolumn)
end)
it('Sets the window options when re-entering oil buffer', function()
oil.open()
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
assert.truthy(vim.w.oil_did_enter)
vim.cmd.edit({ args = { 'README.md' } })
assert.falsy(vim.w.oil_did_enter)
oil.open()
assert.truthy(vim.w.oil_did_enter)
vim.cmd.vsplit()
assert.truthy(vim.w.oil_did_enter)
end)
end)

View file

@ -1,157 +0,0 @@
require("plenary.async").tests.add_to_env()
local fs = require("oil.fs")
local oil = require("oil")
local test_util = require("tests.test_util")
a.describe("Alternate buffer", function()
after_each(function()
test_util.reset_editor()
end)
a.it("sets previous buffer as alternate", function()
vim.cmd.edit({ args = { "foo" } })
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
vim.cmd.edit({ args = { "bar" } })
assert.equals("foo", vim.fn.expand("#"))
end)
a.it("sets previous buffer as alternate when editing url file", function()
vim.cmd.edit({ args = { "foo" } })
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
local readme = fs.join(vim.fn.getcwd(), "README.md")
vim.cmd.edit({ args = { "oil://" .. fs.os_to_posix_path(readme) } })
-- We're gonna jump around to 2 different buffers
test_util.wait_for_autocmd("BufEnter")
test_util.wait_for_autocmd("BufEnter")
assert.equals(readme, vim.api.nvim_buf_get_name(0))
assert.equals("foo", vim.fn.expand("#"))
end)
a.it("sets previous buffer as alternate when editing oil://", function()
vim.cmd.edit({ args = { "foo" } })
vim.cmd.edit({ args = { "oil://" .. fs.os_to_posix_path(vim.fn.getcwd()) } })
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
vim.cmd.edit({ args = { "bar" } })
assert.equals("foo", vim.fn.expand("#"))
end)
a.it("preserves alternate buffer if editing the same file", function()
vim.cmd.edit({ args = { "foo" } })
vim.cmd.edit({ args = { "bar" } })
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
vim.cmd.edit({ args = { "bar" } })
assert.equals("foo", vim.fn.expand("#"))
end)
a.it("preserves alternate buffer if discarding changes", function()
vim.cmd.edit({ args = { "foo" } })
vim.cmd.edit({ args = { "bar" } })
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
oil.close()
assert.equals("bar", vim.fn.expand("%"))
assert.equals("foo", vim.fn.expand("#"))
end)
a.it("sets previous buffer as alternate after multi-dir hops", function()
vim.cmd.edit({ args = { "foo" } })
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
vim.cmd.edit({ args = { "bar" } })
assert.equals("foo", vim.fn.expand("#"))
end)
a.it("sets previous buffer as alternate when inside oil buffer", function()
vim.cmd.edit({ args = { "foo" } })
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("foo", vim.fn.expand("#"))
vim.cmd.edit({ args = { "bar" } })
assert.equals("foo", vim.fn.expand("#"))
oil.open()
assert.equals("bar", vim.fn.expand("#"))
end)
a.it("preserves alternate when traversing oil dirs", function()
vim.cmd.edit({ args = { "foo" } })
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("foo", vim.fn.expand("#"))
vim.wait(1000, function()
return oil.get_cursor_entry()
end, 10)
vim.api.nvim_win_set_cursor(0, { 1, 1 })
oil.select()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("foo", vim.fn.expand("#"))
end)
a.it("preserves alternate when opening preview", function()
vim.cmd.edit({ args = { "foo" } })
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("foo", vim.fn.expand("#"))
vim.wait(1000, function()
return oil.get_cursor_entry()
end, 10)
vim.api.nvim_win_set_cursor(0, { 1, 1 })
oil.open_preview()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("foo", vim.fn.expand("#"))
end)
a.describe("floating window", function()
a.it("sets previous buffer as alternate", function()
vim.cmd.edit({ args = { "foo" } })
oil.open_float()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
-- This is lazy, but testing the actual select logic is more difficult. We can simply
-- replicate it by closing the current window and then doing the edit
vim.api.nvim_win_close(0, true)
vim.cmd.edit({ args = { "bar" } })
assert.equals("foo", vim.fn.expand("#"))
end)
a.it("preserves alternate buffer if editing the same file", function()
vim.cmd.edit({ args = { "foo" } })
vim.cmd.edit({ args = { "bar" } })
oil.open_float()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
-- This is lazy, but testing the actual select logic is more difficult. We can simply
-- replicate it by closing the current window and then doing the edit
vim.api.nvim_win_close(0, true)
vim.cmd.edit({ args = { "bar" } })
assert.equals("foo", vim.fn.expand("#"))
end)
a.it("preserves alternate buffer if discarding changes", function()
vim.cmd.edit({ args = { "foo" } })
vim.cmd.edit({ args = { "bar" } })
oil.open_float()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
oil.close()
assert.equals("foo", vim.fn.expand("#"))
end)
a.it("preserves alternate when traversing to a new file", function()
vim.cmd.edit({ args = { "foo" } })
oil.open_float()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("foo", vim.fn.expand("#"))
test_util.feedkeys({ "/LICENSE<CR>" }, 10)
oil.select()
test_util.wait_for_autocmd("BufEnter")
assert.equals("LICENSE", vim.fn.expand("%:."))
assert.equals("foo", vim.fn.expand("#"))
end)
end)
end)

View file

@ -1,45 +0,0 @@
require("plenary.async").tests.add_to_env()
local oil = require("oil")
local test_util = require("tests.test_util")
a.describe("close", function()
a.before_each(function()
test_util.reset_editor()
end)
a.after_each(function()
test_util.reset_editor()
end)
a.it("does not close buffer from visual mode", function()
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("oil", vim.bo.filetype)
test_util.feedkeys({ "V" }, 10)
oil.close()
assert.equals("oil", vim.bo.filetype)
test_util.feedkeys({ "<Esc>" }, 10)
end)
a.it("does not close buffer from operator-pending mode", function()
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("oil", vim.bo.filetype)
vim.api.nvim_feedkeys("d", "n", false)
a.util.sleep(20)
local mode = vim.api.nvim_get_mode().mode
if mode:match("^no") then
oil.close()
assert.equals("oil", vim.bo.filetype)
end
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", true, true, true), "n", false)
a.util.sleep(20)
end)
a.it("closes buffer from normal mode", function()
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("oil", vim.bo.filetype)
oil.close()
assert.not_equals("oil", vim.bo.filetype)
end)
end)

View file

@ -1,173 +0,0 @@
require("plenary.async").tests.add_to_env()
local TmpDir = require("tests.tmpdir")
local files = require("oil.adapters.files")
local test_util = require("tests.test_util")
a.describe("files adapter", function()
local tmpdir
a.before_each(function()
tmpdir = TmpDir.new()
end)
a.after_each(function()
if tmpdir then
tmpdir:dispose()
end
test_util.reset_editor()
end)
a.it("tmpdir creates files and asserts they exist", function()
tmpdir:create({ "a.txt", "foo/b.txt", "foo/c.txt", "bar/" })
tmpdir:assert_fs({
["a.txt"] = "a.txt",
["foo/b.txt"] = "foo/b.txt",
["foo/c.txt"] = "foo/c.txt",
["bar/"] = true,
})
end)
a.it("Creates files", function()
local err = a.wrap(files.perform_action, 2)({
url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "a.txt",
entry_type = "file",
type = "create",
})
assert.is_nil(err)
tmpdir:assert_fs({
["a.txt"] = "",
})
end)
a.it("Creates directories", function()
local err = a.wrap(files.perform_action, 2)({
url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "a",
entry_type = "directory",
type = "create",
})
assert.is_nil(err)
tmpdir:assert_fs({
["a/"] = true,
})
end)
a.it("Deletes files", function()
tmpdir:create({ "a.txt" })
a.util.scheduler()
local url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "a.txt"
local err = a.wrap(files.perform_action, 2)({
url = url,
entry_type = "file",
type = "delete",
})
assert.is_nil(err)
tmpdir:assert_fs({})
end)
a.it("Deletes directories", function()
tmpdir:create({ "a/" })
local url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "a"
local err = a.wrap(files.perform_action, 2)({
url = url,
entry_type = "directory",
type = "delete",
})
assert.is_nil(err)
tmpdir:assert_fs({})
end)
a.it("Moves files", function()
tmpdir:create({ "a.txt" })
a.util.scheduler()
local src_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "a.txt"
local dest_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "b.txt"
local err = a.wrap(files.perform_action, 2)({
src_url = src_url,
dest_url = dest_url,
entry_type = "file",
type = "move",
})
assert.is_nil(err)
tmpdir:assert_fs({
["b.txt"] = "a.txt",
})
end)
a.it("Moves directories", function()
tmpdir:create({ "a/a.txt" })
a.util.scheduler()
local src_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "a"
local dest_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "b"
local err = a.wrap(files.perform_action, 2)({
src_url = src_url,
dest_url = dest_url,
entry_type = "directory",
type = "move",
})
assert.is_nil(err)
tmpdir:assert_fs({
["b/a.txt"] = "a/a.txt",
["b/"] = true,
})
end)
a.it("Copies files", function()
tmpdir:create({ "a.txt" })
a.util.scheduler()
local src_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "a.txt"
local dest_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "b.txt"
local err = a.wrap(files.perform_action, 2)({
src_url = src_url,
dest_url = dest_url,
entry_type = "file",
type = "copy",
})
assert.is_nil(err)
tmpdir:assert_fs({
["a.txt"] = "a.txt",
["b.txt"] = "a.txt",
})
end)
a.it("Recursively copies directories", function()
tmpdir:create({ "a/a.txt" })
a.util.scheduler()
local src_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "a"
local dest_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "b"
local err = a.wrap(files.perform_action, 2)({
src_url = src_url,
dest_url = dest_url,
entry_type = "directory",
type = "copy",
})
assert.is_nil(err)
tmpdir:assert_fs({
["b/a.txt"] = "a/a.txt",
["b/"] = true,
["a/a.txt"] = "a/a.txt",
["a/"] = true,
})
end)
a.it("Editing a new oil://path/ creates an oil buffer", function()
local tmpdir_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "/"
vim.cmd.edit({ args = { tmpdir_url } })
test_util.wait_oil_ready()
local new_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "newdir"
vim.cmd.edit({ args = { new_url } })
test_util.wait_oil_ready()
assert.equals("oil", vim.bo.filetype)
-- The normalization will add a '/'
assert.equals(new_url .. "/", vim.api.nvim_buf_get_name(0))
end)
a.it("Editing a new oil://file.rb creates a normal buffer", function()
local tmpdir_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "/"
vim.cmd.edit({ args = { tmpdir_url } })
test_util.wait_for_autocmd("BufReadPost")
local new_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "file.rb"
vim.cmd.edit({ args = { new_url } })
test_util.wait_for_autocmd("BufReadPost")
assert.equals("ruby", vim.bo.filetype)
assert.equals(vim.fn.fnamemodify(tmpdir.path, ":p") .. "file.rb", vim.api.nvim_buf_get_name(0))
assert.equals(tmpdir.path .. "/file.rb", vim.fn.bufname())
end)
end)

View file

@ -1,5 +0,0 @@
vim.opt.runtimepath:append(".")
vim.o.swapfile = false
vim.bo.swapfile = false
require("tests.test_util").reset_editor()

View file

@ -1,59 +0,0 @@
local fs = require("oil.fs")
local test_util = require("tests.test_util")
local util = require("oil.util")
describe("update_moved_buffers", function()
after_each(function()
test_util.reset_editor()
end)
it("Renames moved buffers", function()
vim.cmd.edit({ args = { "oil-test:///foo/bar.txt" } })
util.update_moved_buffers("file", "oil-test:///foo/bar.txt", "oil-test:///foo/baz.txt")
assert.equals("oil-test:///foo/baz.txt", vim.api.nvim_buf_get_name(0))
end)
it("Renames moved buffers when they are normal files", function()
local tmpdir = fs.join(vim.loop.fs_realpath(vim.fn.stdpath("cache")), "oil", "test")
local testfile = fs.join(tmpdir, "foo.txt")
vim.cmd.edit({ args = { testfile } })
util.update_moved_buffers(
"file",
"oil://" .. fs.os_to_posix_path(testfile),
"oil://" .. fs.os_to_posix_path(fs.join(tmpdir, "bar.txt"))
)
assert.equals(fs.join(tmpdir, "bar.txt"), vim.api.nvim_buf_get_name(0))
end)
it("Renames directories", function()
vim.cmd.edit({ args = { "oil-test:///foo/" } })
util.update_moved_buffers("directory", "oil-test:///foo/", "oil-test:///bar/")
assert.equals("oil-test:///bar/", vim.api.nvim_buf_get_name(0))
end)
it("Renames subdirectories", function()
vim.cmd.edit({ args = { "oil-test:///foo/bar/" } })
util.update_moved_buffers("directory", "oil-test:///foo/", "oil-test:///baz/")
assert.equals("oil-test:///baz/bar/", vim.api.nvim_buf_get_name(0))
end)
it("Renames subfiles", function()
vim.cmd.edit({ args = { "oil-test:///foo/bar.txt" } })
util.update_moved_buffers("directory", "oil-test:///foo/", "oil-test:///baz/")
assert.equals("oil-test:///baz/bar.txt", vim.api.nvim_buf_get_name(0))
end)
it("Renames subfiles when they are normal files", function()
local tmpdir = fs.join(vim.loop.fs_realpath(vim.fn.stdpath("cache")), "oil", "test")
local foo = fs.join(tmpdir, "foo")
local bar = fs.join(tmpdir, "bar")
local testfile = fs.join(foo, "foo.txt")
vim.cmd.edit({ args = { testfile } })
util.update_moved_buffers(
"directory",
"oil://" .. fs.os_to_posix_path(foo),
"oil://" .. fs.os_to_posix_path(bar)
)
assert.equals(fs.join(bar, "foo.txt"), vim.api.nvim_buf_get_name(0))
end)
end)

View file

@ -1,426 +0,0 @@
require("plenary.async").tests.add_to_env()
local cache = require("oil.cache")
local constants = require("oil.constants")
local mutator = require("oil.mutator")
local test_adapter = require("oil.adapters.test")
local test_util = require("tests.test_util")
local FIELD_ID = constants.FIELD_ID
local FIELD_NAME = constants.FIELD_NAME
local FIELD_TYPE = constants.FIELD_TYPE
a.describe("mutator", function()
after_each(function()
test_util.reset_editor()
end)
describe("build actions", function()
it("empty diffs produce no actions", function()
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
local actions = mutator.create_actions_from_diffs({
[bufnr] = {},
})
assert.are.same({}, actions)
end)
it("constructs CREATE actions", function()
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
local diffs = {
{ type = "new", name = "a.txt", entry_type = "file" },
}
local actions = mutator.create_actions_from_diffs({
[bufnr] = diffs,
})
assert.are.same({
{
type = "create",
entry_type = "file",
url = "oil-test:///foo/a.txt",
},
}, actions)
end)
it("constructs DELETE actions", function()
local file = test_adapter.test_set("/foo/a.txt", "file")
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
local diffs = {
{ type = "delete", name = "a.txt", id = file[FIELD_ID] },
}
local actions = mutator.create_actions_from_diffs({
[bufnr] = diffs,
})
assert.are.same({
{
type = "delete",
entry_type = "file",
url = "oil-test:///foo/a.txt",
},
}, actions)
end)
it("constructs COPY actions", function()
local file = test_adapter.test_set("/foo/a.txt", "file")
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
local diffs = {
{ type = "new", name = "b.txt", entry_type = "file", id = file[FIELD_ID] },
}
local actions = mutator.create_actions_from_diffs({
[bufnr] = diffs,
})
assert.are.same({
{
type = "copy",
entry_type = "file",
src_url = "oil-test:///foo/a.txt",
dest_url = "oil-test:///foo/b.txt",
},
}, actions)
end)
it("constructs MOVE actions", function()
local file = test_adapter.test_set("/foo/a.txt", "file")
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
local diffs = {
{ type = "delete", name = "a.txt", id = file[FIELD_ID] },
{ type = "new", name = "b.txt", entry_type = "file", id = file[FIELD_ID] },
}
local actions = mutator.create_actions_from_diffs({
[bufnr] = diffs,
})
assert.are.same({
{
type = "move",
entry_type = "file",
src_url = "oil-test:///foo/a.txt",
dest_url = "oil-test:///foo/b.txt",
},
}, actions)
end)
it("correctly orders MOVE + CREATE", function()
local file = test_adapter.test_set("/a.txt", "file")
vim.cmd.edit({ args = { "oil-test:///" } })
local bufnr = vim.api.nvim_get_current_buf()
local diffs = {
{ type = "delete", name = "a.txt", id = file[FIELD_ID] },
{ type = "new", name = "b.txt", entry_type = "file", id = file[FIELD_ID] },
{ type = "new", name = "a.txt", entry_type = "file" },
}
local actions = mutator.create_actions_from_diffs({
[bufnr] = diffs,
})
assert.are.same({
{
type = "move",
entry_type = "file",
src_url = "oil-test:///a.txt",
dest_url = "oil-test:///b.txt",
},
{
type = "create",
entry_type = "file",
url = "oil-test:///a.txt",
},
}, actions)
end)
it("resolves MOVE loops", function()
local afile = test_adapter.test_set("/a.txt", "file")
local bfile = test_adapter.test_set("/b.txt", "file")
vim.cmd.edit({ args = { "oil-test:///" } })
local bufnr = vim.api.nvim_get_current_buf()
local diffs = {
{ type = "delete", name = "a.txt", id = afile[FIELD_ID] },
{ type = "new", name = "b.txt", entry_type = "file", id = afile[FIELD_ID] },
{ type = "delete", name = "b.txt", id = bfile[FIELD_ID] },
{ type = "new", name = "a.txt", entry_type = "file", id = bfile[FIELD_ID] },
}
math.randomseed(2983982)
local actions = mutator.create_actions_from_diffs({
[bufnr] = diffs,
})
local tmp_url = "oil-test:///a.txt__oil_tmp_510852"
assert.are.same({
{
type = "move",
entry_type = "file",
src_url = "oil-test:///a.txt",
dest_url = tmp_url,
},
{
type = "move",
entry_type = "file",
src_url = "oil-test:///b.txt",
dest_url = "oil-test:///a.txt",
},
{
type = "move",
entry_type = "file",
src_url = tmp_url,
dest_url = "oil-test:///b.txt",
},
}, actions)
end)
end)
describe("order actions", function()
it("Creates files inside dir before move", function()
local move = {
type = "move",
src_url = "oil-test:///a",
dest_url = "oil-test:///b",
entry_type = "directory",
}
local create = { type = "create", url = "oil-test:///a/hi.txt", entry_type = "file" }
local actions = { move, create }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ create, move }, ordered_actions)
end)
it("Moves file out of parent before deleting parent", function()
local move = {
type = "move",
src_url = "oil-test:///a/b.txt",
dest_url = "oil-test:///b.txt",
entry_type = "file",
}
local delete = { type = "delete", url = "oil-test:///a", entry_type = "directory" }
local actions = { delete, move }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ move, delete }, ordered_actions)
end)
it("Handles parent child move ordering", function()
-- move parent into a child and child OUT of parent
-- MOVE /a/b -> /b
-- MOVE /a -> /b/a
local move1 = {
type = "move",
src_url = "oil-test:///a/b",
dest_url = "oil-test:///b",
entry_type = "directory",
}
local move2 = {
type = "move",
src_url = "oil-test:///a",
dest_url = "oil-test:///b/a",
entry_type = "directory",
}
local actions = { move2, move1 }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ move1, move2 }, ordered_actions)
end)
it("Handles a delete inside a moved folder", function()
-- delete in directory and move directory
-- DELETE /a/b.txt
-- MOVE /a/ -> /b/
local del = {
type = "delete",
url = "oil-test:///a/b.txt",
entry_type = "file",
}
local move = {
type = "move",
src_url = "oil-test:///a",
dest_url = "oil-test:///b",
entry_type = "directory",
}
local actions = { move, del }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ del, move }, ordered_actions)
end)
it("Detects move directory loops", function()
local move = {
type = "move",
src_url = "oil-test:///a",
dest_url = "oil-test:///a/b",
entry_type = "directory",
}
assert.has_error(function()
mutator.enforce_action_order({ move })
end)
end)
it("Detects copy directory loops", function()
local move = {
type = "copy",
src_url = "oil-test:///a",
dest_url = "oil-test:///a/b",
entry_type = "directory",
}
assert.has_error(function()
mutator.enforce_action_order({ move })
end)
end)
it("Detects nested copy directory loops", function()
local move = {
type = "copy",
src_url = "oil-test:///a",
dest_url = "oil-test:///a/b/a",
entry_type = "directory",
}
assert.has_error(function()
mutator.enforce_action_order({ move })
end)
end)
describe("change", function()
it("applies CHANGE after CREATE", function()
local create = { type = "create", url = "oil-test:///a/hi.txt", entry_type = "file" }
local change = {
type = "change",
url = "oil-test:///a/hi.txt",
entry_type = "file",
column = "TEST",
value = "TEST",
}
local actions = { change, create }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ create, change }, ordered_actions)
end)
it("applies CHANGE after COPY src", function()
local copy = {
type = "copy",
src_url = "oil-test:///a/hi.txt",
dest_url = "oil-test:///b.txt",
entry_type = "file",
}
local change = {
type = "change",
url = "oil-test:///a/hi.txt",
entry_type = "file",
column = "TEST",
value = "TEST",
}
local actions = { change, copy }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ copy, change }, ordered_actions)
end)
it("applies CHANGE after COPY dest", function()
local copy = {
type = "copy",
src_url = "oil-test:///b.txt",
dest_url = "oil-test:///a/hi.txt",
entry_type = "file",
}
local change = {
type = "change",
url = "oil-test:///a/hi.txt",
entry_type = "file",
column = "TEST",
value = "TEST",
}
local actions = { change, copy }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ copy, change }, ordered_actions)
end)
it("applies CHANGE after MOVE dest", function()
local move = {
type = "move",
src_url = "oil-test:///b.txt",
dest_url = "oil-test:///a/hi.txt",
entry_type = "file",
}
local change = {
type = "change",
url = "oil-test:///a/hi.txt",
entry_type = "file",
column = "TEST",
value = "TEST",
}
local actions = { change, move }
local ordered_actions = mutator.enforce_action_order(actions)
assert.are.same({ move, change }, ordered_actions)
end)
end)
end)
a.describe("perform actions", function()
a.it("creates new entries", function()
local actions = {
{ type = "create", url = "oil-test:///a.txt", entry_type = "file" },
}
a.wrap(mutator.process_actions, 2)(actions)
local files = cache.list_url("oil-test:///")
assert.are.same({
["a.txt"] = {
[FIELD_ID] = 1,
[FIELD_TYPE] = "file",
[FIELD_NAME] = "a.txt",
},
}, files)
end)
a.it("deletes entries", function()
local file = test_adapter.test_set("/a.txt", "file")
local actions = {
{ type = "delete", url = "oil-test:///a.txt", entry_type = "file" },
}
a.wrap(mutator.process_actions, 2)(actions)
local files = cache.list_url("oil-test:///")
assert.are.same({}, files)
assert.is_nil(cache.get_entry_by_id(file[FIELD_ID]))
assert.has_error(function()
cache.get_parent_url(file[FIELD_ID])
end)
end)
a.it("moves entries", function()
local file = test_adapter.test_set("/a.txt", "file")
local actions = {
{
type = "move",
src_url = "oil-test:///a.txt",
dest_url = "oil-test:///b.txt",
entry_type = "file",
},
}
a.wrap(mutator.process_actions, 2)(actions)
local files = cache.list_url("oil-test:///")
local new_entry = {
[FIELD_ID] = file[FIELD_ID],
[FIELD_TYPE] = "file",
[FIELD_NAME] = "b.txt",
}
assert.are.same({
["b.txt"] = new_entry,
}, files)
assert.are.same(new_entry, cache.get_entry_by_id(file[FIELD_ID]))
assert.equals("oil-test:///", cache.get_parent_url(file[FIELD_ID]))
end)
a.it("copies entries", function()
local file = test_adapter.test_set("/a.txt", "file")
local actions = {
{
type = "copy",
src_url = "oil-test:///a.txt",
dest_url = "oil-test:///b.txt",
entry_type = "file",
},
}
a.wrap(mutator.process_actions, 2)(actions)
local files = cache.list_url("oil-test:///")
local new_entry = {
[FIELD_ID] = file[FIELD_ID] + 1,
[FIELD_TYPE] = "file",
[FIELD_NAME] = "b.txt",
}
assert.are.same({
["a.txt"] = file,
["b.txt"] = new_entry,
}, files)
end)
end)
end)

View file

@ -1,250 +0,0 @@
require("plenary.async").tests.add_to_env()
local constants = require("oil.constants")
local parser = require("oil.mutator.parser")
local test_adapter = require("oil.adapters.test")
local test_util = require("tests.test_util")
local util = require("oil.util")
local view = require("oil.view")
local FIELD_ID = constants.FIELD_ID
local FIELD_META = constants.FIELD_META
local function set_lines(bufnr, lines)
vim.bo[bufnr].modifiable = true
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
end
describe("parser", function()
after_each(function()
test_util.reset_editor()
end)
it("detects new files", function()
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
"a.txt",
})
local diffs = parser.parse(bufnr)
assert.are.same({ { entry_type = "file", name = "a.txt", type = "new" } }, diffs)
end)
it("detects new directories", function()
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
"foo/",
})
local diffs = parser.parse(bufnr)
assert.are.same({ { entry_type = "directory", name = "foo", type = "new" } }, diffs)
end)
it("detects new links", function()
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
"a.txt -> b.txt",
})
local diffs = parser.parse(bufnr)
assert.are.same(
{ { entry_type = "link", name = "a.txt", type = "new", link = "b.txt" } },
diffs
)
end)
it("detects deleted files", function()
local file = test_adapter.test_set("/foo/a.txt", "file")
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {})
local diffs = parser.parse(bufnr)
assert.are.same({
{ name = "a.txt", type = "delete", id = file[FIELD_ID] },
}, diffs)
end)
it("detects deleted directories", function()
local dir = test_adapter.test_set("/foo/bar", "directory")
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {})
local diffs = parser.parse(bufnr)
assert.are.same({
{ name = "bar", type = "delete", id = dir[FIELD_ID] },
}, diffs)
end)
it("detects deleted links", function()
local file = test_adapter.test_set("/foo/a.txt", "link")
file[FIELD_META] = { link = "b.txt" }
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {})
local diffs = parser.parse(bufnr)
assert.are.same({
{ name = "a.txt", type = "delete", id = file[FIELD_ID] },
}, diffs)
end)
it("ignores empty lines", function()
local file = test_adapter.test_set("/foo/a.txt", "file")
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
local cols = view.format_entry_cols(file, {}, {}, test_adapter, false)
local lines = util.render_table({ cols }, {})
table.insert(lines, "")
table.insert(lines, " ")
set_lines(bufnr, lines)
local diffs = parser.parse(bufnr)
assert.are.same({}, diffs)
end)
it("errors on missing filename", function()
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
"/008",
})
local _, errors = parser.parse(bufnr)
assert.are_same({
{
message = "Malformed ID at start of line",
lnum = 0,
end_lnum = 1,
col = 0,
},
}, errors)
end)
it("errors on empty dirname", function()
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
"/008 /",
})
local _, errors = parser.parse(bufnr)
assert.are.same({
{
message = "No filename found",
lnum = 0,
end_lnum = 1,
col = 0,
},
}, errors)
end)
it("errors on duplicate names", function()
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
"foo",
"foo/",
})
local _, errors = parser.parse(bufnr)
assert.are.same({
{
message = "Duplicate filename",
lnum = 1,
end_lnum = 2,
col = 0,
},
}, errors)
end)
it("errors on duplicate names for existing files", function()
local file = test_adapter.test_set("/foo/a.txt", "file")
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
"a.txt",
string.format("/%d a.txt", file[FIELD_ID]),
})
local _, errors = parser.parse(bufnr)
assert.are.same({
{
message = "Duplicate filename",
lnum = 1,
end_lnum = 2,
col = 0,
},
}, errors)
end)
it("ignores new dirs with empty name", function()
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
"/",
})
local diffs = parser.parse(bufnr)
assert.are.same({}, diffs)
end)
it("parses a rename as a delete + new", function()
local file = test_adapter.test_set("/foo/a.txt", "file")
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
string.format("/%d b.txt", file[FIELD_ID]),
})
local diffs = parser.parse(bufnr)
assert.are.same({
{ type = "new", id = file[FIELD_ID], name = "b.txt", entry_type = "file" },
{ type = "delete", id = file[FIELD_ID], name = "a.txt" },
}, diffs)
end)
it("detects a new trailing slash as a delete + create", function()
local file = test_adapter.test_set("/foo", "file")
vim.cmd.edit({ args = { "oil-test:///" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
string.format("/%d foo/", file[FIELD_ID]),
})
local diffs = parser.parse(bufnr)
assert.are.same({
{ type = "new", name = "foo", entry_type = "directory" },
{ type = "delete", id = file[FIELD_ID], name = "foo" },
}, diffs)
end)
it("detects renamed files that conflict", function()
local afile = test_adapter.test_set("/foo/a.txt", "file")
local bfile = test_adapter.test_set("/foo/b.txt", "file")
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
string.format("/%d a.txt", bfile[FIELD_ID]),
string.format("/%d b.txt", afile[FIELD_ID]),
})
local diffs = parser.parse(bufnr)
local first_two = { diffs[1], diffs[2] }
local last_two = { diffs[3], diffs[4] }
table.sort(first_two, function(a, b)
return a.id < b.id
end)
table.sort(last_two, function(a, b)
return a.id < b.id
end)
assert.are.same({
{ name = "b.txt", type = "new", id = afile[FIELD_ID], entry_type = "file" },
{ name = "a.txt", type = "new", id = bfile[FIELD_ID], entry_type = "file" },
}, first_two)
assert.are.same({
{ name = "a.txt", type = "delete", id = afile[FIELD_ID] },
{ name = "b.txt", type = "delete", id = bfile[FIELD_ID] },
}, last_two)
end)
it("views link targets with trailing slashes as the same", function()
local file = test_adapter.test_set("/foo/mydir", "link")
file[FIELD_META] = { link = "dir/" }
vim.cmd.edit({ args = { "oil-test:///foo/" } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
string.format("/%d mydir/ -> dir/", file[FIELD_ID]),
})
local diffs = parser.parse(bufnr)
assert.are.same({}, diffs)
end)
end)

View file

@ -1,148 +0,0 @@
require("plenary.async").tests.add_to_env()
local TmpDir = require("tests.tmpdir")
local actions = require("oil.actions")
local oil = require("oil")
local test_util = require("tests.test_util")
local view = require("oil.view")
a.describe("regression tests", function()
local tmpdir
a.before_each(function()
tmpdir = TmpDir.new()
end)
a.after_each(function()
if tmpdir then
tmpdir:dispose()
tmpdir = nil
end
test_util.reset_editor()
end)
-- see https://github.com/stevearc/oil.nvim/issues/25
a.it("can edit dirs that will be renamed to an existing buffer", function()
vim.cmd.edit({ args = { "README.md" } })
vim.cmd.vsplit()
vim.cmd.edit({ args = { "%:p:h" } })
assert.equals("oil", vim.bo.filetype)
vim.cmd.wincmd({ args = { "p" } })
assert.equals("markdown", vim.bo.filetype)
vim.cmd.edit({ args = { "%:p:h" } })
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.equals("oil", vim.bo.filetype)
end)
-- https://github.com/stevearc/oil.nvim/issues/37
a.it("places the cursor on correct entry when opening on file", function()
vim.cmd.edit({ args = { "." } })
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
local entry = oil.get_cursor_entry()
assert.not_nil(entry)
assert.not_equals("README.md", entry and entry.name)
vim.cmd.edit({ args = { "README.md" } })
view.delete_hidden_buffers()
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
entry = oil.get_cursor_entry()
assert.equals("README.md", entry and entry.name)
end)
-- https://github.com/stevearc/oil.nvim/issues/64
a.it("doesn't close floating windows oil didn't open itself", function()
local winid = vim.api.nvim_open_win(vim.fn.bufadd("README.md"), true, {
relative = "editor",
row = 1,
col = 1,
width = 100,
height = 100,
})
oil.open()
a.util.sleep(10)
oil.close()
a.util.sleep(10)
assert.equals(winid, vim.api.nvim_get_current_win())
end)
-- https://github.com/stevearc/oil.nvim/issues/64
a.it("doesn't close splits on oil.close", function()
vim.cmd.edit({ args = { "README.md" } })
vim.cmd.vsplit()
local winid = vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_get_current_buf()
oil.open()
a.util.sleep(10)
oil.close()
a.util.sleep(10)
assert.equals(2, #vim.api.nvim_tabpage_list_wins(0))
assert.equals(winid, vim.api.nvim_get_current_win())
assert.equals(bufnr, vim.api.nvim_get_current_buf())
end)
-- https://github.com/stevearc/oil.nvim/issues/79
a.it("Returns to empty buffer on close", function()
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
oil.close()
assert.not_equals("oil", vim.bo.filetype)
assert.equals("", vim.api.nvim_buf_get_name(0))
end)
a.it("All buffers set nomodified after save", function()
tmpdir:create({ "a.txt" })
a.util.scheduler()
vim.cmd.edit({ args = { "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") } })
local first_dir = vim.api.nvim_get_current_buf()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
test_util.feedkeys({ "dd", "itest/<esc>", "<CR>" }, 10)
vim.wait(1000, function()
return vim.bo.modifiable
end, 10)
test_util.feedkeys({ "p" }, 10)
a.util.scheduler()
oil.save({ confirm = false })
vim.wait(1000, function()
return vim.bo.modifiable
end, 10)
tmpdir:assert_fs({
["test/a.txt"] = "a.txt",
})
-- The first oil buffer should not be modified anymore
assert.falsy(vim.bo[first_dir].modified)
end)
a.it("refreshing buffer doesn't lose track of it", function()
vim.cmd.edit({ args = { "." } })
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
local bufnr = vim.api.nvim_get_current_buf()
vim.cmd.edit({ bang = true })
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.are.same({ bufnr }, require("oil.view").get_all_buffers())
end)
a.it("can copy a file multiple times", function()
test_util.actions.open({ tmpdir.path })
vim.api.nvim_feedkeys("ifoo.txt", "x", true)
test_util.actions.save()
vim.api.nvim_feedkeys("yyp$ciWbar.txt", "x", true)
vim.api.nvim_feedkeys("yyp$ciWbaz.txt", "x", true)
test_util.actions.save()
assert.are.same({ "bar.txt", "baz.txt", "foo.txt" }, test_util.parse_entries(0))
tmpdir:assert_fs({
["foo.txt"] = "",
["bar.txt"] = "",
["baz.txt"] = "",
})
end)
-- https://github.com/stevearc/oil.nvim/issues/355
a.it("can open files from floating window", function()
tmpdir:create({ "a.txt" })
a.util.scheduler()
oil.open_float(tmpdir.path)
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
actions.select.callback()
vim.wait(1000, function()
return vim.fn.expand("%:t") == "a.txt"
end, 10)
assert.equals("a.txt", vim.fn.expand("%:t"))
end)
end)

View file

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

View file

@ -1,25 +0,0 @@
local oil = require("oil")
local util = require("oil.util")
describe("url", function()
it("get_url_for_path", function()
local cases = {
{ "", "oil://" .. util.addslash(vim.fn.getcwd()) },
{ "term://~/oil.nvim//52953:/bin/sh", "oil://" .. vim.loop.os_homedir() .. "/oil.nvim/" },
{ "/foo/bar.txt", "oil:///foo/", "bar.txt" },
{ "oil:///foo/bar.txt", "oil:///foo/", "bar.txt" },
{ "oil:///", "oil:///" },
{ "oil-ssh://user@hostname:8888//bar.txt", "oil-ssh://user@hostname:8888//", "bar.txt" },
{ "oil-ssh://user@hostname:8888//", "oil-ssh://user@hostname:8888//" },
}
for _, case in ipairs(cases) do
local input, expected, expected_basename = unpack(case)
local output, basename = oil.get_buffer_parent_url(input, true)
assert.equals(expected, output, string.format('Parent url for path "%s" failed', input))
assert.equals(
expected_basename,
basename,
string.format('Basename for path "%s" failed', input)
)
end
end)
end)

View file

@ -1,66 +0,0 @@
require("plenary.async").tests.add_to_env()
local oil = require("oil")
local test_util = require("tests.test_util")
a.describe("window options", function()
after_each(function()
test_util.reset_editor()
end)
a.it("Restores window options on close", function()
vim.cmd.edit({ args = { "README.md" } })
test_util.oil_open()
assert.equals("no", vim.o.signcolumn)
oil.close()
assert.equals("auto", vim.o.signcolumn)
end)
a.it("Restores window options on edit", function()
test_util.oil_open()
assert.equals("no", vim.o.signcolumn)
vim.cmd.edit({ args = { "README.md" } })
assert.equals("auto", vim.o.signcolumn)
end)
a.it("Restores window options on split <filename>", function()
test_util.oil_open()
assert.equals("no", vim.o.signcolumn)
vim.cmd.split({ args = { "README.md" } })
assert.equals("auto", vim.o.signcolumn)
end)
a.it("Restores window options on split", function()
test_util.oil_open()
assert.equals("no", vim.o.signcolumn)
vim.cmd.split()
vim.cmd.edit({ args = { "README.md" } })
assert.equals("auto", vim.o.signcolumn)
end)
a.it("Restores window options on tabnew <filename>", function()
test_util.oil_open()
assert.equals("no", vim.o.signcolumn)
vim.cmd.tabnew({ args = { "README.md" } })
assert.equals("auto", vim.o.signcolumn)
end)
a.it("Restores window options on tabnew", function()
test_util.oil_open()
assert.equals("no", vim.o.signcolumn)
vim.cmd.tabnew()
vim.cmd.edit({ args = { "README.md" } })
assert.equals("auto", vim.o.signcolumn)
end)
a.it("Sets the window options when re-entering oil buffer", function()
oil.open()
test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
assert.truthy(vim.w.oil_did_enter)
vim.cmd.edit({ args = { "README.md" } })
assert.falsy(vim.w.oil_did_enter)
oil.open()
assert.truthy(vim.w.oil_did_enter)
vim.cmd.vsplit()
assert.truthy(vim.w.oil_did_enter)
end)
end)

36
vim.toml Normal file
View file

@ -0,0 +1,36 @@
[selene]
base = "lua51"
name = "vim"
[vim]
any = true
[jit]
any = true
[bit]
any = true
[assert]
any = true
[a]
any = true
[describe]
any = true
[it]
any = true
[before_each]
any = true
[after_each]
any = true
[spy]
any = true
[stub]
any = true