diff --git a/.envrc b/.envrc index d522e34..175de89 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1 @@ -export VIRTUAL_ENV=venv layout python -python -c 'import pyparsing' 2>/dev/null || pip install -r scripts/requirements.txt diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 327aaf2..2c4cc29 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -33,16 +33,6 @@ body: description: A clear and concise description of what the bug is. Please include any related errors you see in Neovim. validations: required: true - - type: dropdown - attributes: - label: What is the severity of this bug? - options: - - minor (annoyance) - - tolerable (can work around it) - - breaking (some functionality is broken) - - blocking (cannot use plugin) - validations: - required: true - type: textarea attributes: label: Steps To Reproduce diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 9916836..41c3459 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -26,15 +26,6 @@ body: placeholder: I am trying to do X. My current workflow is Y. validations: required: false - - type: dropdown - attributes: - label: What is the significance of this feature? - options: - - nice to have - - strongly desired - - cannot use this plugin without it - validations: - required: true - type: textarea attributes: label: Additional details diff --git a/.github/generate.py b/.github/generate.py new file mode 100755 index 0000000..87881c5 --- /dev/null +++ b/.github/generate.py @@ -0,0 +1,227 @@ +import os +import os.path +import re +from dataclasses import dataclass, field +from typing import List + +from nvim_doc_tools import ( + LuaParam, + Vimdoc, + VimdocSection, + generate_md_toc, + indent, + leftright, + parse_functions, + read_nvim_json, + read_section, + render_md_api, + render_vimdoc_api, + replace_section, + wrap, +) +from nvim_doc_tools.vimdoc import format_vimdoc_params + +HERE = os.path.dirname(__file__) +ROOT = os.path.abspath(os.path.join(HERE, os.path.pardir)) +README = os.path.join(ROOT, "README.md") +DOC = os.path.join(ROOT, "doc") +VIMDOC = os.path.join(DOC, "oil.txt") + + +def add_md_link_path(path: str, lines: List[str]) -> List[str]: + ret = [] + for line in lines: + ret.append(re.sub(r"(\(#)", "(" + path + "#", line)) + return ret + + +def update_md_api(): + api_doc = os.path.join(DOC, "api.md") + funcs = parse_functions(os.path.join(ROOT, "lua", "oil", "init.lua")) + lines = ["\n"] + render_md_api(funcs, 2) + ["\n"] + replace_section( + api_doc, + r"^$", + r"^$", + lines, + ) + toc = ["\n"] + generate_md_toc(api_doc, max_level=1) + ["\n"] + replace_section( + api_doc, + r"^$", + r"^$", + toc, + ) + toc = add_md_link_path("doc/api.md", toc) + replace_section( + README, + r"^$", + r"^$", + toc, + ) + + +def update_readme_toc(): + toc = ["\n"] + generate_md_toc(README, max_level=1) + ["\n"] + replace_section( + README, + r"^$", + r"^$", + toc, + ) + + +def update_config_options(): + config_file = os.path.join(ROOT, "lua", "oil", "config.lua") + opt_lines = read_section(config_file, r"^\s*local default_config =", r"^}$") + replace_section( + README, + r"^require\(\"oil\"\)\.setup\(\{$", + r"^}\)$", + opt_lines, + ) + + +@dataclass +class ColumnDef: + name: str + adapters: str + editable: bool + summary: str + params: List["LuaParam"] = field(default_factory=list) + + +HL = [ + LuaParam( + "highlight", + "string|fun(value: string): string", + "Highlight group, or function that returns a highlight group", + ) +] +TIME = [ + LuaParam("format", "string", "Format string (see :help strftime)"), +] +COL_DEFS = [ + ColumnDef( + "type", + "*", + False, + "The type of the entry (file, directory, link, etc)", + HL + + [LuaParam("icons", "table", "Mapping of entry type to icon")], + ), + ColumnDef( + "icon", + "*", + False, + "An icon for the entry's type (requires nvim-web-devicons)", + HL + + [ + LuaParam("default_file", "string", "Fallback icon for files when nvim-web-devicons returns nil"), + LuaParam("directory", "string", "Icon for directories"), + LuaParam("add_padding", "boolean", "Set to false to remove the extra whitespace after the icon"), + ], + ), + ColumnDef("size", "files, ssh", False, "The size of the file", HL + []), + ColumnDef( + "permissions", "files, ssh", True, "Access permissions of the file", HL + [] + ), + ColumnDef("ctime", "files", False, "Change timestamp of the file", HL + TIME + []), + ColumnDef( + "mtime", "files", False, "Last modified time of the file", HL + TIME + [] + ), + ColumnDef("atime", "files", False, "Last access time of the file", HL + TIME + []), + ColumnDef( + "birthtime", "files", False, "The time the file was created", HL + TIME + [] + ), +] + + +def get_options_vimdoc() -> "VimdocSection": + section = VimdocSection("options", "oil-options") + config_file = os.path.join(ROOT, "lua", "oil", "config.lua") + opt_lines = read_section(config_file, r"^local default_config =", r"^}$") + lines = ["\n", ">\n", ' require("oil").setup({\n'] + lines.extend(indent(opt_lines, 4)) + lines.extend([" })\n", "<\n"]) + section.body = lines + return section + + +def get_highlights_vimdoc() -> "VimdocSection": + section = VimdocSection("Highlights", "oil-highlights", ["\n"]) + highlights = read_nvim_json('require("oil")._get_highlights()') + for hl in highlights: + name = hl["name"] + desc = hl.get("desc") + if desc is None: + continue + section.body.append(leftright(name, f"*hl-{name}*")) + section.body.extend(wrap(desc, 4)) + section.body.append("\n") + return section + + +def get_actions_vimdoc() -> "VimdocSection": + section = VimdocSection("Actions", "oil-actions", ["\n"]) + section.body.extend( + wrap( + "These are actions that can be used in the `keymaps` section of config options." + ) + ) + section.body.append("\n") + actions = read_nvim_json('require("oil.actions")._get_actions()') + actions.sort(key=lambda a: a["name"]) + for action in actions: + name = action["name"] + desc = action["desc"] + section.body.append(leftright(name, f"*actions.{name}*")) + section.body.extend(wrap(desc, 4)) + section.body.append("\n") + return section + + +def get_columns_vimdoc() -> "VimdocSection": + section = VimdocSection("Columns", "oil-columns", ["\n"]) + section.body.extend( + wrap( + 'Columns can be specified as a string to use default arguments (e.g. `"icon"`), or as a table to pass parameters (e.g. `{"size", highlight = "Special"}`)' + ) + ) + section.body.append("\n") + for col in COL_DEFS: + section.body.append(leftright(col.name, f"*column-{col.name}*")) + section.body.extend(wrap(f"Adapters: {col.adapters}", 4)) + if col.editable: + section.body.extend(wrap(f"Editable: this column is read/write", 4)) + section.body.extend(wrap(col.summary, 4)) + section.body.append("\n") + section.body.append(" Parameters:\n") + section.body.extend(format_vimdoc_params(col.params, 6)) + section.body.append("\n") + return section + + +def generate_vimdoc(): + doc = Vimdoc("oil.txt", "oil") + funcs = parse_functions(os.path.join(ROOT, "lua", "oil", "init.lua")) + doc.sections.extend( + [ + get_options_vimdoc(), + VimdocSection("API", "oil-api", render_vimdoc_api("oil", funcs)), + get_columns_vimdoc(), + get_actions_vimdoc(), + get_highlights_vimdoc(), + ] + ) + + with open(VIMDOC, "w", encoding="utf-8") as ofile: + ofile.writelines(doc.render()) + + +def main() -> None: + """Update the README""" + update_config_options() + update_md_api() + update_readme_toc() + generate_vimdoc() diff --git a/scripts/main.py b/.github/main.py similarity index 100% rename from scripts/main.py rename to .github/main.py diff --git a/.github/nvim_doc_tools b/.github/nvim_doc_tools new file mode 160000 index 0000000..d146f2b --- /dev/null +++ b/.github/nvim_doc_tools @@ -0,0 +1 @@ +Subproject commit d146f2b7e72892b748e21d40a175267ce2ac1b7b diff --git a/.github/pre-commit b/.github/pre-commit index c64fbec..49ee249 100755 --- a/.github/pre-commit +++ b/.github/pre-commit @@ -1,3 +1,5 @@ #!/bin/bash set -e -make fastlint +luacheck lua tests + +stylua --check . diff --git a/.github/pre-push b/.github/pre-push deleted file mode 100755 index ecb23a9..0000000 --- a/.github/pre-push +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -set -e -IFS=' ' -while read local_ref _local_sha _remote_ref _remote_sha; do - remote_main=$( (git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo "///master") | cut -f 4 -d / | tr -d "[:space:]") - local_ref_short=$(echo "$local_ref" | cut -f 3 -d / | tr -d "[:space:]") - if [ "$local_ref_short" = "$remote_main" ]; then - make lint - make test - fi -done diff --git a/.github/workflows/automation_remove_question_label_on_comment.yml b/.github/workflows/automation_remove_question_label_on_comment.yml deleted file mode 100644 index f99bba8..0000000 --- a/.github/workflows/automation_remove_question_label_on_comment.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Remove Question Label on Issue Comment - -on: [issue_comment] - -jobs: - # Remove the "question" label when a new comment is added. - # This lets me ask a question, tag the issue with "question", and filter out all "question"-tagged - # issues in my "needs triage" filter. - remove_question: - runs-on: ubuntu-latest - if: github.event.sender.login != 'stevearc' - steps: - - uses: actions/checkout@v4 - - uses: actions-ecosystem/action-remove-labels@v1 - with: - labels: question diff --git a/.github/workflows/automation_request_review.yml b/.github/workflows/automation_request_review.yml deleted file mode 100644 index c31f582..0000000 --- a/.github/workflows/automation_request_review.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Request Review -permissions: - pull-requests: write -on: - pull_request_target: - types: [opened, reopened, ready_for_review, synchronize] - branches-ignore: - - "release-please--**" - -jobs: - # Request review automatically when PRs are opened - request_review: - runs-on: ubuntu-latest - steps: - - name: Request Review - uses: actions/github-script@v7 - if: github.actor != 'stevearc' - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - const pr = context.payload.pull_request; - github.rest.pulls.requestReviewers({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: pr.number, - reviewers: ['stevearc'] - }); diff --git a/.github/workflows/install_nvim.sh b/.github/workflows/install_nvim.sh index 9ba2d26..c5119dc 100644 --- a/.github/workflows/install_nvim.sh +++ b/.github/workflows/install_nvim.sh @@ -1,16 +1,12 @@ #!/bin/bash set -e -version="${NVIM_TAG-stable}" -dl_name="nvim-linux-x86_64.appimage" -# The appimage name changed in v0.10.4 -if python -c 'from packaging.version import Version; import sys; sys.exit(not (Version(sys.argv[1]) < Version("v0.10.4")))' "$version" 2>/dev/null; then - dl_name="nvim.appimage" -fi -curl -sL "https://github.com/neovim/neovim/releases/download/${version}/${dl_name}" -o nvim.appimage +PLUGINS="$HOME/.local/share/nvim/site/pack/plugins/start" +mkdir -p "$PLUGINS" + +wget "https://github.com/neovim/neovim/releases/download/${NVIM_TAG}/nvim.appimage" chmod +x nvim.appimage ./nvim.appimage --appimage-extract >/dev/null rm -f nvim.appimage mkdir -p ~/.local/share/nvim mv squashfs-root ~/.local/share/nvim/appimage sudo ln -s "$HOME/.local/share/nvim/appimage/AppRun" /usr/bin/nvim -/usr/bin/nvim --version diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2b62853..a512819 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,20 +1,13 @@ name: Tests -on: - push: - branches: - - master - - stevearc-* - pull_request: - branches: - - master +on: [push, pull_request] jobs: luacheck: name: Luacheck runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - name: Prepare run: | @@ -24,44 +17,33 @@ jobs: sudo luarocks install luacheck - name: Run Luacheck - run: luacheck lua tests + run: luacheck . stylua: name: StyLua runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - name: Stylua - uses: JohnnyMorganz/stylua-action@v4 + uses: JohnnyMorganz/stylua-action@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - version: v2.0.2 - args: --check lua tests - - typecheck: - name: typecheck - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - uses: stevearc/nvim-typecheck-action@v2 - with: - path: lua + version: v0.15.2 + args: --check . run_tests: strategy: matrix: include: - nvim_tag: v0.8.3 - - nvim_tag: v0.9.4 - - nvim_tag: v0.10.4 - - nvim_tag: v0.11.0 + - nvim_tag: v0.9.1 name: Run tests runs-on: ubuntu-22.04 env: NVIM_TAG: ${{ matrix.nvim_tag }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - name: Install Neovim and dependencies run: | @@ -71,35 +53,6 @@ jobs: run: | bash ./run_tests.sh - update_docs: - name: Update docs - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - - name: Install Neovim and dependencies - run: | - bash ./.github/workflows/install_nvim.sh - - - name: Update docs - run: | - python -m pip install pyparsing==3.0.9 - make doc - - name: Commit changes - if: ${{ github.ref == 'refs/heads/master' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COMMIT_MSG: | - [docgen] Update docs - skip-checks: true - run: | - git config user.email "actions@github" - git config user.name "Github Actions" - git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git - git add README.md doc - # Only commit and push if we have changes - git diff --quiet && git diff --staged --quiet || (git commit -m "${COMMIT_MSG}"; git push origin HEAD:${GITHUB_REF}) - release: name: release @@ -107,16 +60,15 @@ jobs: needs: - luacheck - stylua - - typecheck - run_tests - - update_docs runs-on: ubuntu-22.04 steps: - - uses: googleapis/release-please-action@v4 + - uses: google-github-actions/release-please-action@v3 id: release with: release-type: simple - - uses: actions/checkout@v4 + package-name: oil.nvim + - uses: actions/checkout@v3 - uses: rickstaa/action-create-tag@v1 if: ${{ steps.release.outputs.release_created }} with: diff --git a/.github/workflows/update-docs.yml b/.github/workflows/update-docs.yml new file mode 100644 index 0000000..c7463c7 --- /dev/null +++ b/.github/workflows/update-docs.yml @@ -0,0 +1,35 @@ +name: Update docs + +on: push + +jobs: + update-readme: + name: Update docs + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + with: + submodules: true + + - name: Install Neovim and dependencies + env: + NVIM_TAG: v0.9.0 + run: | + bash ./.github/workflows/install_nvim.sh + + - name: Update docs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_MSG: | + [docgen] Update docs + skip-checks: true + run: | + git config user.email "actions@github" + git config user.name "Github Actions" + git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git + python -m pip install pyparsing==3.0.9 + python .github/main.py generate + python .github/main.py lint + git add README.md doc + # Only commit and push if we have changes + git diff --quiet && git diff --staged --quiet || (git commit -m "${COMMIT_MSG}"; git push origin HEAD:${GITHUB_REF}) diff --git a/.gitignore b/.gitignore index d427c40..d818abb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,6 @@ luac.out *.zip *.tar.gz -# python bytecode -__pycache__ - # Object files *.o *.os @@ -44,10 +41,4 @@ __pycache__ .direnv/ .testenv/ -venv/ doc/tags -scripts/nvim_doc_tools -scripts/nvim-typecheck-action -scripts/benchmark.nvim -perf/tmp/ -profile.json diff --git a/.gitmodules b/.gitmodules index e69de29..c47e568 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule ".github/nvim_doc_tools"] + path = .github/nvim_doc_tools + url = https://github.com/stevearc/nvim_doc_tools diff --git a/.luarc.json b/.luarc.json deleted file mode 100644 index 68da2f2..0000000 --- a/.luarc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "runtime": { - "version": "LuaJIT", - "pathStrict": true - }, - "type": { - "checkTableShape": true - } -} diff --git a/.stylua.toml b/.stylua.toml index 020ce91..3cfeffd 100644 --- a/.stylua.toml +++ b/.stylua.toml @@ -1,5 +1,3 @@ column_width = 100 indent_type = "Spaces" indent_width = 2 -[sort_requires] -enabled = true diff --git a/CHANGELOG.md b/CHANGELOG.md index 458b3cb..97c6fb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,386 +1,5 @@ # Changelog -## [2.15.0](https://github.com/stevearc/oil.nvim/compare/v2.14.0...v2.15.0) (2025-02-13) - - -### Features - -* add support for bufnr in column rendering functions ([#575](https://github.com/stevearc/oil.nvim/issues/575)) ([8abc58b](https://github.com/stevearc/oil.nvim/commit/8abc58b038f84078121ab1cac6ecad0163fe1635)) -* API to automatically open preview window after opening oil ([#339](https://github.com/stevearc/oil.nvim/issues/339)) ([57528bf](https://github.com/stevearc/oil.nvim/commit/57528bf9c58080ca891e8d362d0a578895c136ce)) -* can selectively add entries to quickfix ([#564](https://github.com/stevearc/oil.nvim/issues/564)) ([b594b9a](https://github.com/stevearc/oil.nvim/commit/b594b9a9052618669ccf6520b2d0c0d942eb8118)) -* floating window max width/height can be percentages ([#553](https://github.com/stevearc/oil.nvim/issues/553)) ([1df90fa](https://github.com/stevearc/oil.nvim/commit/1df90faf927e78f5aacf278abd0bfdcb5f45e825)) -* most moves and copies will copy the undofile ([#583](https://github.com/stevearc/oil.nvim/issues/583)) ([32dd3e3](https://github.com/stevearc/oil.nvim/commit/32dd3e378d47673679e76a773451f82f971a66df)) -* pass oil bufnr to custom filename highlight function ([#552](https://github.com/stevearc/oil.nvim/issues/552)) ([f5c563a](https://github.com/stevearc/oil.nvim/commit/f5c563a074a38cee5a09f98e98b74dcd2c322490)) - - -### Bug Fixes - -* crash in preview on nvim 0.8 ([81b2c5f](https://github.com/stevearc/oil.nvim/commit/81b2c5f04ae24a8c83b20ecbd017fecac15faca0)) -* directory rendering with custom highlights ([#551](https://github.com/stevearc/oil.nvim/issues/551)) ([a6a4f48](https://github.com/stevearc/oil.nvim/commit/a6a4f48b14b4a51fded531c86f6c04b4503a2ef8)) -* disable_preview respected when preview_method != "load" ([#577](https://github.com/stevearc/oil.nvim/issues/577)) ([7cde5aa](https://github.com/stevearc/oil.nvim/commit/7cde5aab10f564408e9ac349d457d755422d58cd)) -* error when non-current oil buffer has validation errors ([#561](https://github.com/stevearc/oil.nvim/issues/561)) ([8d11a2a](https://github.com/stevearc/oil.nvim/commit/8d11a2abf3039b1974d4acd65fbc83ada2ca1084)) -* gracefully handle fs_stat failures ([#558](https://github.com/stevearc/oil.nvim/issues/558)) ([7c26a59](https://github.com/stevearc/oil.nvim/commit/7c26a59ac0061b199bf9f44b19d45cfadd9b14f5)) -* guard against nil metadata values ([#548](https://github.com/stevearc/oil.nvim/issues/548)) ([254bc66](https://github.com/stevearc/oil.nvim/commit/254bc6635cb3f77e6e9a89155652f368e5535160)) -* more consistent cursor position when entering a new directory ([#536](https://github.com/stevearc/oil.nvim/issues/536)) ([c80fa5c](https://github.com/stevearc/oil.nvim/commit/c80fa5c415b882c1c694a32748cea09b7dafc2c5)) -* more robust parsing of custom column timestamp formats ([#582](https://github.com/stevearc/oil.nvim/issues/582)) ([5313690](https://github.com/stevearc/oil.nvim/commit/5313690956d27cc6b53d5a2583df05e717c59b16)) -* open files in correct window from floating oil ([#560](https://github.com/stevearc/oil.nvim/issues/560)) ([83ac518](https://github.com/stevearc/oil.nvim/commit/83ac5185f79ab8d869bccea792dc516ad02ad06e)) -* preview sometimes causes oil buffers to be stuck in unloaded state ([#563](https://github.com/stevearc/oil.nvim/issues/563)) ([1488f0d](https://github.com/stevearc/oil.nvim/commit/1488f0d96b1cb820dd12f05a7bf5283a631a7c4d)) -* stat files if fs_readdir doesn't provide a type ([#543](https://github.com/stevearc/oil.nvim/issues/543)) ([c6a39a6](https://github.com/stevearc/oil.nvim/commit/c6a39a69b2df7c10466f150dde0bd23e49c1fba3)) -* support permissions checks on windows and virtual filesystems ([#555](https://github.com/stevearc/oil.nvim/issues/555)) ([7041528](https://github.com/stevearc/oil.nvim/commit/7041528bdedb350ad66e650684deec8456e053cc)) -* work around incorrect link detection on windows ([#557](https://github.com/stevearc/oil.nvim/issues/557)) ([09fa1d2](https://github.com/stevearc/oil.nvim/commit/09fa1d22f5edf0730824d2b222d726c8c81bbdc9)) - -## [2.14.0](https://github.com/stevearc/oil.nvim/compare/v2.13.0...v2.14.0) (2024-12-21) - - -### Features - -* add `win_options` to `preview_win` ([#514](https://github.com/stevearc/oil.nvim/issues/514)) ([bbeed86](https://github.com/stevearc/oil.nvim/commit/bbeed86bde134da8d09bed64b6aa0d65642e6b23)) -* add highlight group for orphaned links ([#502](https://github.com/stevearc/oil.nvim/issues/502)) ([740b8fd](https://github.com/stevearc/oil.nvim/commit/740b8fd425a2b77f7f40eb5ac155ebe66ff9515c)) -* better merging of action desc when overriding keymaps ([f2b3249](https://github.com/stevearc/oil.nvim/commit/f2b324933f4d505cff6f7d445fd61fad02dcd9ae)) -* config option to customize filename highlight group ([#508](https://github.com/stevearc/oil.nvim/issues/508)) ([99ce32f](https://github.com/stevearc/oil.nvim/commit/99ce32f4a2ecf76263b72fcc31efb163faa1a941)) -* config option to disable previewing a file ([3fa3161](https://github.com/stevearc/oil.nvim/commit/3fa3161aa9515ff6a7cf7e44458b6a2114262870)) -* disable preview for large files ([#511](https://github.com/stevearc/oil.nvim/issues/511)) ([c23fe08](https://github.com/stevearc/oil.nvim/commit/c23fe08e0546d9efc242e19f0d829efa7e7b2743)) -* highlight groups for hidden files ([#459](https://github.com/stevearc/oil.nvim/issues/459)) ([60e6896](https://github.com/stevearc/oil.nvim/commit/60e68967e51ff1ecd264c29e3de0d52bfff22df3)) -* option to quite vim if oil is closed as last buffer ([#491](https://github.com/stevearc/oil.nvim/issues/491)) ([81cc9c3](https://github.com/stevearc/oil.nvim/commit/81cc9c3f62ddbef3687931d119e505643496fa0a)) -* use scratch buffer for file previews ([#467](https://github.com/stevearc/oil.nvim/issues/467)) ([21705a1](https://github.com/stevearc/oil.nvim/commit/21705a1debe6d85a53c138ab944484b685432b2b)) - - -### Bug Fixes - -* cursor sometimes does not hover previous file ([8ea40b5](https://github.com/stevearc/oil.nvim/commit/8ea40b5506115b6d355e304dd9ee5089f7d78601)) -* don't take over the preview window until it's opened for oil ([#532](https://github.com/stevearc/oil.nvim/issues/532)) ([78ab7ca](https://github.com/stevearc/oil.nvim/commit/78ab7ca1073731ebdf82efa474202defa028d5a4)) -* handle files with newlines in the name ([#534](https://github.com/stevearc/oil.nvim/issues/534)) ([dba0375](https://github.com/stevearc/oil.nvim/commit/dba037598843973b8c54bc5ce0318db4a0da439d)) -* image.nvim previews with preview_method=scratch ([5acab3d](https://github.com/stevearc/oil.nvim/commit/5acab3d8a9bc85a571688db432f2702dd7d901a4)) -* improper file name escaping ([#530](https://github.com/stevearc/oil.nvim/issues/530)) ([7a55ede](https://github.com/stevearc/oil.nvim/commit/7a55ede5e745e31ea8e4cb5483221524922294bf)) -* set alternate when using floating windows ([#526](https://github.com/stevearc/oil.nvim/issues/526)) ([c5f7c56](https://github.com/stevearc/oil.nvim/commit/c5f7c56644425e2b77e71904da98cda0331b3342)) -* work around performance issue with treesitter, folds, and large directories ([da93d55](https://github.com/stevearc/oil.nvim/commit/da93d55e32d73a17c447067d168d80290ae96590)) - - -### Performance Improvements - -* change default view_options.natural_order behavior to disable on large directories ([01b0b9d](https://github.com/stevearc/oil.nvim/commit/01b0b9d8ef79b7b631e92f6b5fed1c639262d570)) -* only sort entries after we have them all ([792f0db](https://github.com/stevearc/oil.nvim/commit/792f0db6ba8b626b14bc127e1ce7247185b3be91)) -* optimize rendering cadence ([c96f93d](https://github.com/stevearc/oil.nvim/commit/c96f93d894cc97e76b0871bec4058530eee8ece4)) -* replace vim.endswith and vim.startswith with string.match ([4de3025](https://github.com/stevearc/oil.nvim/commit/4de30256c32cd272482bc6df0c6de78ffc389153)) - -## [2.13.0](https://github.com/stevearc/oil.nvim/compare/v2.12.2...v2.13.0) (2024-11-11) - - -### Features - -* config option to customize floating window title ([#482](https://github.com/stevearc/oil.nvim/issues/482)) ([5d2dfae](https://github.com/stevearc/oil.nvim/commit/5d2dfae655b9b689bd4017b3bdccd52cbee5b92f)) -* config option to disable lsp file methods ([#477](https://github.com/stevearc/oil.nvim/issues/477)) ([f60bb7f](https://github.com/stevearc/oil.nvim/commit/f60bb7f793477d99ef1acf39e920bf2ca4e644de)) - - -### Bug Fixes - -* actions.preview accepts options ([#497](https://github.com/stevearc/oil.nvim/issues/497)) ([cca1631](https://github.com/stevearc/oil.nvim/commit/cca1631d5ea450c09ba72f3951a9e28105a3632c)) -* add trailing slash to directories on yank_entry ([#504](https://github.com/stevearc/oil.nvim/issues/504)) ([42333bb](https://github.com/stevearc/oil.nvim/commit/42333bb46e34dd47e13927010b1dcd30e6e4ca96)) -* don't deep merge keymaps ([#510](https://github.com/stevearc/oil.nvim/issues/510)) ([709403c](https://github.com/stevearc/oil.nvim/commit/709403ccd6f22d859c2e42c780ab558ae89284d9)) -* guard against nil keymaps ([621f8ba](https://github.com/stevearc/oil.nvim/commit/621f8ba4fa821724e9b646732a26fb2e795fe008)) -* only map ~ for normal mode ([#484](https://github.com/stevearc/oil.nvim/issues/484)) ([ccab9d5](https://github.com/stevearc/oil.nvim/commit/ccab9d5e09e2d0042fbbe5b6bd05e82426247067)) -* sort keymap help entries by description ([#506](https://github.com/stevearc/oil.nvim/issues/506)) ([52cc8a1](https://github.com/stevearc/oil.nvim/commit/52cc8a1fb35ea6ce1df536143add7ce7215c63c0)), closes [#376](https://github.com/stevearc/oil.nvim/issues/376) - -## [2.12.2](https://github.com/stevearc/oil.nvim/compare/v2.12.1...v2.12.2) (2024-09-10) - - -### Bug Fixes - -* ensure win_options are being set on correct window ([#469](https://github.com/stevearc/oil.nvim/issues/469)) ([30e0438](https://github.com/stevearc/oil.nvim/commit/30e0438ff08f197d7ce4a417445ab97ee72efe2d)) -* wrap git rm callback in schedule_wrap ([#475](https://github.com/stevearc/oil.nvim/issues/475)) ([b053744](https://github.com/stevearc/oil.nvim/commit/b05374428e5136d9b6c8e1e8e62a75f82283b1f8)) - - -### Performance Improvements - -* **view:** avoid running `is_hidden_file` when `show_hidden` is set ([#471](https://github.com/stevearc/oil.nvim/issues/471)) ([0fcd126](https://github.com/stevearc/oil.nvim/commit/0fcd1263a2e8b6200e2b9fd4ab83d40ed8899c54)) - -## [2.12.1](https://github.com/stevearc/oil.nvim/compare/v2.12.0...v2.12.1) (2024-08-26) - - -### Bug Fixes - -* gracefully handle trashing file that does not exist ([70337eb](https://github.com/stevearc/oil.nvim/commit/70337eb77f53cbff0b7f54f403d5b2b0a9430935)) -* process deletes in dir before moving dir ([349bca8](https://github.com/stevearc/oil.nvim/commit/349bca8c3eae4ab78629ed63ee55cc3458a367c0)) - -## [2.12.0](https://github.com/stevearc/oil.nvim/compare/v2.11.0...v2.12.0) (2024-08-17) - - -### Features - -* add support for `mini.icons` ([#439](https://github.com/stevearc/oil.nvim/issues/439)) ([a543ea5](https://github.com/stevearc/oil.nvim/commit/a543ea598eaef3363fe253e0e11837c1404eb04d)) -* allow bufnr optional parameter for get_current_dir function ([#440](https://github.com/stevearc/oil.nvim/issues/440)) ([cc23325](https://github.com/stevearc/oil.nvim/commit/cc2332599f8944076fba29ff7960729b3fcdd71b)) -* disable cursor in preview window ([#433](https://github.com/stevearc/oil.nvim/issues/433)) ([b15e4c1](https://github.com/stevearc/oil.nvim/commit/b15e4c1e647b9ddbb75a31caeb720b3b3ce4db54)) - - -### Bug Fixes - -* add compatibility for Lua 5.1 ([#456](https://github.com/stevearc/oil.nvim/issues/456)) ([b39a789](https://github.com/stevearc/oil.nvim/commit/b39a78959f3f69e9c1bf43c2634bbddf0af51c3e)) -* correctly check if `mini.icons` is actually setup ([#441](https://github.com/stevearc/oil.nvim/issues/441)) ([d5e5657](https://github.com/stevearc/oil.nvim/commit/d5e56574f896120b78cdf56dc1132e76057f8877)) -* cursor sometimes disappears after making changes ([#438](https://github.com/stevearc/oil.nvim/issues/438)) ([b5a1abf](https://github.com/stevearc/oil.nvim/commit/b5a1abfde00eead6814cae3321e4c90ff98cfff1)) -* Force standard C locale when getting `ls` input for parsing in SSH ([#455](https://github.com/stevearc/oil.nvim/issues/455)) ([71c972f](https://github.com/stevearc/oil.nvim/commit/71c972fbd218723a3c15afcb70421f67340f5a6d)) -* handle rare case where file watcher outlives buffer ([fcca212](https://github.com/stevearc/oil.nvim/commit/fcca212c2e966fc3dec1d4baf888e670631d25d1)) -* Handle users and groups with spaces over SSH ([#448](https://github.com/stevearc/oil.nvim/issues/448)) ([a6cea1a](https://github.com/stevearc/oil.nvim/commit/a6cea1a5b9bc9351769fe09a547c62fe4b669abd)) -* set floating window win_options when buffer changes ([#432](https://github.com/stevearc/oil.nvim/issues/432)) ([b0a6cf9](https://github.com/stevearc/oil.nvim/commit/b0a6cf98982cdcf82b19b0029b734bbbcd24bcc4)) - -## [2.11.0](https://github.com/stevearc/oil.nvim/compare/v2.10.0...v2.11.0) (2024-07-01) - - -### Features - -* case insensitive sorting ([#429](https://github.com/stevearc/oil.nvim/issues/429)) ([2077cc3](https://github.com/stevearc/oil.nvim/commit/2077cc3358f327aca16c376cdde6ea0b07f14449)) -* rename experimental_watch_for_changes -> watch_for_changes ([c7c7ce5](https://github.com/stevearc/oil.nvim/commit/c7c7ce5bd47030ee9c60a859f25695647610b8bd)) -* support preview from floating window ([#403](https://github.com/stevearc/oil.nvim/issues/403)) ([59b3dab](https://github.com/stevearc/oil.nvim/commit/59b3dab6f79e147a0d694ee72c26ae883d323340)) - - -### Bug Fixes - -* bug in buffer rendering race condition handling ([f6df58a](https://github.com/stevearc/oil.nvim/commit/f6df58ad370f45dbc18c42ffbaefbcf27df14036)) -* correctly check group permissions in unix ([#428](https://github.com/stevearc/oil.nvim/issues/428)) ([65c53db](https://github.com/stevearc/oil.nvim/commit/65c53dbe4f2140236590a7568a5f22a77d16be39)) -* increase loading display delay to avoid flicker ([#424](https://github.com/stevearc/oil.nvim/issues/424)) ([4c574cf](https://github.com/stevearc/oil.nvim/commit/4c574cf4a2de736d2662d52ce086d8bdf87c49df)) - -## [2.10.0](https://github.com/stevearc/oil.nvim/compare/v2.9.0...v2.10.0) (2024-06-16) - - -### Features - -* add copy filename action ([#391](https://github.com/stevearc/oil.nvim/issues/391)) ([bbc0e67](https://github.com/stevearc/oil.nvim/commit/bbc0e67eebc15342e73b146a50d9b52e6148161b)) -* keymap actions can be parameterized ([96368e1](https://github.com/stevearc/oil.nvim/commit/96368e13e9b1aaacc570e4825b8787307f0d05e1)) - - -### Bug Fixes - -* change unknown action name from error to notification ([e5eb20e](https://github.com/stevearc/oil.nvim/commit/e5eb20e88fc03bf89f371032de77f176158b41d3)) -* error opening command window from oil float ([#378](https://github.com/stevearc/oil.nvim/issues/378)) ([06a19f7](https://github.com/stevearc/oil.nvim/commit/06a19f77f1a1da37b675635e6f9c5b5d50bcaacd)) -* hack around glob issues in LSP rename operations ([#386](https://github.com/stevearc/oil.nvim/issues/386)) ([e5312c3](https://github.com/stevearc/oil.nvim/commit/e5312c3a801e7274fa14e6a56aa10a618fed80c3)) -* incorrect default config actions ([#414](https://github.com/stevearc/oil.nvim/issues/414)) ([c82b26e](https://github.com/stevearc/oil.nvim/commit/c82b26eb4ba35c0eb7ec38d88dd400597fb34883)) -* notify when changing the current directory ([#406](https://github.com/stevearc/oil.nvim/issues/406)) ([18272ab](https://github.com/stevearc/oil.nvim/commit/18272aba9d00a3176a5443d50dbb4464acc167bd)) -* throw error on vim.has call within the lsp/workspace.lua ([#411](https://github.com/stevearc/oil.nvim/issues/411)) ([61f1967](https://github.com/stevearc/oil.nvim/commit/61f1967222365474c6cf7953c569cc94dbcc7acd)) -* vim.notify call error ([76bfc25](https://github.com/stevearc/oil.nvim/commit/76bfc25520e4edc98d089d023b4ed06013639849)) - -## [2.9.0](https://github.com/stevearc/oil.nvim/compare/v2.8.0...v2.9.0) (2024-05-16) - - -### Features - -* can restore Oil progress window when minimized ([fa3820e](https://github.com/stevearc/oil.nvim/commit/fa3820ebf1e8ccf5c7c0f3626d499b2c1aa8bc50)) -* experimental support for git operations ([#290](https://github.com/stevearc/oil.nvim/issues/290)) ([1f05774](https://github.com/stevearc/oil.nvim/commit/1f05774e1c2dbc1940104b5c950d5c7b65ec6e0b)) - - -### Bug Fixes - -* duplicate create actions ([#334](https://github.com/stevearc/oil.nvim/issues/334)) ([354c530](https://github.com/stevearc/oil.nvim/commit/354c53080a6d7f4f0b2f0cc12e53bede2480b9e5)) -* error when opening files from floating oil window ([#355](https://github.com/stevearc/oil.nvim/issues/355)) ([2bc56ad](https://github.com/stevearc/oil.nvim/commit/2bc56ad68afd092af1b2e77dd5d61e156938564c)) -* git mv errors when moving empty directory ([#358](https://github.com/stevearc/oil.nvim/issues/358)) ([6a7a10b](https://github.com/stevearc/oil.nvim/commit/6a7a10b6117aface6a25b54906140ad4f7fdabfc)) -* gracefully handle new dirs with trailing backslash on windows ([#336](https://github.com/stevearc/oil.nvim/issues/336)) ([be0a1ec](https://github.com/stevearc/oil.nvim/commit/be0a1ecbf0541692a1b9b6e8ea15f5f57db8747a)) -* icon column highlight parameter ([#366](https://github.com/stevearc/oil.nvim/issues/366)) ([752563c](https://github.com/stevearc/oil.nvim/commit/752563c59d64a5764cc0743d4fa0aac9ae4a2640)) -* race condition when entering oil buffer ([#321](https://github.com/stevearc/oil.nvim/issues/321)) ([c86e484](https://github.com/stevearc/oil.nvim/commit/c86e48407b8a45f9aa8acb2b4512b384ea1eec84)) -* **ssh:** bad argument when editing files over ssh ([#370](https://github.com/stevearc/oil.nvim/issues/370)) ([aa0c00c](https://github.com/stevearc/oil.nvim/commit/aa0c00c7fd51982ac476d165cd021f348cf5ea71)) -* **ssh:** config option to pass extra args to SCP ([#340](https://github.com/stevearc/oil.nvim/issues/340)) ([3abb607](https://github.com/stevearc/oil.nvim/commit/3abb6077d7d6b09f5eb794b8764223b3027f6807)) -* **ssh:** garbled output when directory has broken symlinks ([bcfc0a2](https://github.com/stevearc/oil.nvim/commit/bcfc0a2e01def5019aa14fac2fc6de20dedb6d3d)) -* support visual mode when preview window is open ([#315](https://github.com/stevearc/oil.nvim/issues/315)) ([f41d7e7](https://github.com/stevearc/oil.nvim/commit/f41d7e7cd8e4028b03c35d847b4396790ac8bb2d)) -* **windows:** convert posix paths before matching LSP watch globs ([#374](https://github.com/stevearc/oil.nvim/issues/374)) ([f630887](https://github.com/stevearc/oil.nvim/commit/f630887cd845a7341bc16488fe8aaecffe3aaa8a)) -* **windows:** file operation preview uses only backslash path separator ([#336](https://github.com/stevearc/oil.nvim/issues/336)) ([96f0983](https://github.com/stevearc/oil.nvim/commit/96f0983e754694e592d4313f583cd31eaebfa80d)) -* **windows:** navigating into drive letter root directories ([#341](https://github.com/stevearc/oil.nvim/issues/341)) ([f3a31eb](https://github.com/stevearc/oil.nvim/commit/f3a31eba24587bc038592103d8f7e64648292115)) -* **windows:** treat both backslash and frontslash as path separators ([#336](https://github.com/stevearc/oil.nvim/issues/336)) ([3b3a6b2](https://github.com/stevearc/oil.nvim/commit/3b3a6b23a120e69ddc980c9d32840ecd521fbff9)) - -## [2.8.0](https://github.com/stevearc/oil.nvim/compare/v2.7.0...v2.8.0) (2024-04-19) - - -### Features - -* add user autocmds before and after performing actions ([#310](https://github.com/stevearc/oil.nvim/issues/310)) ([e462a34](https://github.com/stevearc/oil.nvim/commit/e462a3446505185adf063566f5007771b69027a1)) - - -### Bug Fixes - -* output suppressed when opening files ([#348](https://github.com/stevearc/oil.nvim/issues/348)) ([6c48ac7](https://github.com/stevearc/oil.nvim/commit/6c48ac7dc679c5694a2c0375a5e67773e31d8157)) -* **ssh:** escape all file paths for the ssh adapter ([#353](https://github.com/stevearc/oil.nvim/issues/353)) ([8bb35eb](https://github.com/stevearc/oil.nvim/commit/8bb35eb81a48f14c4a1ef480c2bbb87ceb7cd8bb)) - -## [2.7.0](https://github.com/stevearc/oil.nvim/compare/v2.6.1...v2.7.0) (2024-03-13) - - -### Features - -* add ability to alter lsp file operation timeout ([#317](https://github.com/stevearc/oil.nvim/issues/317)) ([29a06fc](https://github.com/stevearc/oil.nvim/commit/29a06fcc906f57894c1bc768219ba590e03d1121)) -* add border config for SSH and keymaps help window ([#299](https://github.com/stevearc/oil.nvim/issues/299)) ([e27cc4e](https://github.com/stevearc/oil.nvim/commit/e27cc4e13812f96c0851de67015030a823cc0fbd)) -* do not close preview when switching dirs ([#277](https://github.com/stevearc/oil.nvim/issues/277)) ([bf753c3](https://github.com/stevearc/oil.nvim/commit/bf753c3e3f8736939ad5597f92329dfe7b1df4f5)) -* experimental option to watch directory for changes ([#292](https://github.com/stevearc/oil.nvim/issues/292)) ([bcfe7d1](https://github.com/stevearc/oil.nvim/commit/bcfe7d1ec5bbf41dd78726f579a363028d208c1a)) -* use natural sort order by default ([#328](https://github.com/stevearc/oil.nvim/issues/328)) ([71b076b](https://github.com/stevearc/oil.nvim/commit/71b076b3afb40663222564c74162db555aeee62d)) - - -### Bug Fixes - -* actions.open_external uses explorer.exe in WSL ([#273](https://github.com/stevearc/oil.nvim/issues/273)) ([6953c2c](https://github.com/stevearc/oil.nvim/commit/6953c2c17d8ae7454b28c44c8767eebede312e6f)) -* close preview window when leaving oil buffer ([#296](https://github.com/stevearc/oil.nvim/issues/296)) ([132b4ea](https://github.com/stevearc/oil.nvim/commit/132b4ea0740c417b9d717411cab4cf187e1fd095)) -* correctly reset bufhidden for formerly previewed buffers ([#291](https://github.com/stevearc/oil.nvim/issues/291)) ([0de8e60](https://github.com/stevearc/oil.nvim/commit/0de8e60e3d8d3d1ff9378526b4722f1ea326e1cb)) -* potential leak in experimental file watcher ([c437f3c](https://github.com/stevearc/oil.nvim/commit/c437f3c5b0da0a9cc6a222d87212cce11b80ba75)) -* spurious exits from faulty :wq detection ([#221](https://github.com/stevearc/oil.nvim/issues/221)) ([e045ee3](https://github.com/stevearc/oil.nvim/commit/e045ee3b4e06cafd7a6a2acac10f2558e611eaf8)) -* window options sometimes not set in oil buffer ([#287](https://github.com/stevearc/oil.nvim/issues/287)) ([17d71eb](https://github.com/stevearc/oil.nvim/commit/17d71eb3d88a79dbc87c6245f8490853a5c38092)) -* **windows:** can delete non-ascii filenames to trash ([#323](https://github.com/stevearc/oil.nvim/issues/323)) ([18dfd24](https://github.com/stevearc/oil.nvim/commit/18dfd2458dc741fea683357a17aaa95870b25a3c)) - -## [2.6.1](https://github.com/stevearc/oil.nvim/compare/v2.6.0...v2.6.1) (2024-01-16) - - -### Bug Fixes - -* crash when LSP client workspace_folders is nil ([#269](https://github.com/stevearc/oil.nvim/issues/269)) ([c4cc824](https://github.com/stevearc/oil.nvim/commit/c4cc8240f1c71defcb67c45da96e44b968d29e5f)) -* diagnostic float would not open if scope=cursor ([#275](https://github.com/stevearc/oil.nvim/issues/275)) ([a1af7a1](https://github.com/stevearc/oil.nvim/commit/a1af7a1b593d8d28581ef0de82a6977721601afa)) -* **lsp_rename:** handle absolute path glob filters ([#279](https://github.com/stevearc/oil.nvim/issues/279)) ([ec24334](https://github.com/stevearc/oil.nvim/commit/ec24334471e7ccbfb7488910159245dc7327a07d)) -* **trash:** mac error deleting dangling symbolic links to trash ([#251](https://github.com/stevearc/oil.nvim/issues/251)) ([49b2b3f](https://github.com/stevearc/oil.nvim/commit/49b2b3f4a50bcd546decf751e5834de9b6f38d97)) - - -### Performance Improvements - -* **windows:** use a single powershell process for trash operations ([#271](https://github.com/stevearc/oil.nvim/issues/271)) ([e71b6ca](https://github.com/stevearc/oil.nvim/commit/e71b6caa95bd29225536df64fdcd8fb0f758bb09)) - -## [2.6.0](https://github.com/stevearc/oil.nvim/compare/v2.5.0...v2.6.0) (2024-01-03) - - -### Features - -* **trash:** support for deleting to windows recycle bin ([#243](https://github.com/stevearc/oil.nvim/issues/243)) ([553b7a0](https://github.com/stevearc/oil.nvim/commit/553b7a0ac129c0e7a7bbde72f9fbfe7c9f4be6c3)) - -## [2.5.0](https://github.com/stevearc/oil.nvim/compare/v2.4.1...v2.5.0) (2023-12-26) - - -### Features - -* actions for sending oil entries to quickfix ([#249](https://github.com/stevearc/oil.nvim/issues/249)) ([3ffb830](https://github.com/stevearc/oil.nvim/commit/3ffb8309e6eda961c7edb9ecbe6a340fe9e24b43)) -* add 'update_on_cursor_moved' option to preview window ([#250](https://github.com/stevearc/oil.nvim/issues/250)) ([ea612fe](https://github.com/stevearc/oil.nvim/commit/ea612fe926a24ea20b2b3856e1ba60bdaaae9383)) -* allow multiple hlgroups inside one column ([#240](https://github.com/stevearc/oil.nvim/issues/240)) ([a173b57](https://github.com/stevearc/oil.nvim/commit/a173b5776c66a31ce08552677c1eae7ab015835f)) -* constrain_cursor option (closes [#257](https://github.com/stevearc/oil.nvim/issues/257)) ([71b1ef5](https://github.com/stevearc/oil.nvim/commit/71b1ef5edfcee7c58fe611fdd79bfafcb9fb0531)) -* option to auto-save files affected by will_rename_files ([#218](https://github.com/stevearc/oil.nvim/issues/218)) ([48d8ea8](https://github.com/stevearc/oil.nvim/commit/48d8ea8f4a6590ef7339ff0fdb97cef3e238dd86)) -* refresh action also clears search highlight ([#228](https://github.com/stevearc/oil.nvim/issues/228)) ([8283457](https://github.com/stevearc/oil.nvim/commit/82834573bbca27c240f30087ff642b807ed1872a)) -* support all LSP workspace file operations ([#264](https://github.com/stevearc/oil.nvim/issues/264)) ([250e0af](https://github.com/stevearc/oil.nvim/commit/250e0af7a54d750792be8b1d6165b76b6603a867)) - - -### Bug Fixes - -* constrain cursor when entering insert mode ([a60c6d1](https://github.com/stevearc/oil.nvim/commit/a60c6d10fd66de275c1d00451c918104ef9b6d10)) -* handle opening oil from buffers with foreign schemes ([#256](https://github.com/stevearc/oil.nvim/issues/256)) ([22ab2ce](https://github.com/stevearc/oil.nvim/commit/22ab2ce1d56832588a634e7737404d9344698bd3)) -* **trash:** error deleting dangling symbolic links to trash ([#251](https://github.com/stevearc/oil.nvim/issues/251)) ([5d9e436](https://github.com/stevearc/oil.nvim/commit/5d9e4368d49aec00b1e0d9ea520e1403ad6ad634)) -* willRename source path ([#248](https://github.com/stevearc/oil.nvim/issues/248)) ([24027ed](https://github.com/stevearc/oil.nvim/commit/24027ed8d7f3ee5c38cfd713915e2e16d89e79b3)) - - -### Performance Improvements - -* speed up session loading ([#246](https://github.com/stevearc/oil.nvim/issues/246)) ([b3c24f4](https://github.com/stevearc/oil.nvim/commit/b3c24f4b3b2d38483241292a330cd6eb00734dac)) - -## [2.4.1](https://github.com/stevearc/oil.nvim/compare/v2.4.0...v2.4.1) (2023-12-01) - - -### Bug Fixes - -* buffer data cleared when setting buflisted = false ([303f318](https://github.com/stevearc/oil.nvim/commit/303f31895e7ce10df250c88c7a5f7d8d9c56f0fc)) -* bug copying file multiple times ([05cb825](https://github.com/stevearc/oil.nvim/commit/05cb8257cb9257144e63f41ccfe5a41ba3d1003c)) -* crash in ssh and trash adapter detail columns ([#235](https://github.com/stevearc/oil.nvim/issues/235)) ([e89a8f8](https://github.com/stevearc/oil.nvim/commit/e89a8f8adeef2dfab851fd056d38ee7afc97c249)) -* oil.select respects splitbelow and splitright ([#233](https://github.com/stevearc/oil.nvim/issues/233)) ([636989b](https://github.com/stevearc/oil.nvim/commit/636989b603fb95032efa9d3e1b3323c8bb533e91)) -* preserve buflisted when re-opening oil buffers ([#220](https://github.com/stevearc/oil.nvim/issues/220)) ([6566f45](https://github.com/stevearc/oil.nvim/commit/6566f457e44498adc6835bed5402b38386fa1438)) - -## [2.4.0](https://github.com/stevearc/oil.nvim/compare/v2.3.0...v2.4.0) (2023-11-15) - - -### Features - -* display ../ entry in oil buffers ([#166](https://github.com/stevearc/oil.nvim/issues/166)) ([d8f0d91](https://github.com/stevearc/oil.nvim/commit/d8f0d91b10ec53da722b0909697b57c2f5368245)) -* trash support for linux and mac ([#165](https://github.com/stevearc/oil.nvim/issues/165)) ([6175bd6](https://github.com/stevearc/oil.nvim/commit/6175bd646272335c8db93264760760d8f2a611d5)) - - -### Bug Fixes - -* can view drives on Windows ([126a8a2](https://github.com/stevearc/oil.nvim/commit/126a8a23465312683edf646555b3031bfe56796d)) -* don't set buflisted on oil buffers ([#220](https://github.com/stevearc/oil.nvim/issues/220)) ([873d505](https://github.com/stevearc/oil.nvim/commit/873d505e5bfdd65317ea97ead8faa6c56bac04c0)) -* line parsing for empty columns ([0715f1b](https://github.com/stevearc/oil.nvim/commit/0715f1b0aacef70573ed6300c12039831fbd81c3)) -* previewing and editing files on windows ([#214](https://github.com/stevearc/oil.nvim/issues/214)) ([3727410](https://github.com/stevearc/oil.nvim/commit/3727410e4875ad8ba339c585859a9391d643b9ed)) -* quit after mutations when :wq or similar ([#221](https://github.com/stevearc/oil.nvim/issues/221)) ([af13ce3](https://github.com/stevearc/oil.nvim/commit/af13ce333f89c54a47e6772b55fed2438ee6957c)) - -## [2.3.0](https://github.com/stevearc/oil.nvim/compare/v2.2.0...v2.3.0) (2023-11-04) - - -### Features - -* add support for LSP willRenameFiles ([#184](https://github.com/stevearc/oil.nvim/issues/184)) ([8f3c1d2](https://github.com/stevearc/oil.nvim/commit/8f3c1d2d2e4f7b81d19f353c61cb4ccba6a26496)) -* make buffer cleanup delay configurable ([#191](https://github.com/stevearc/oil.nvim/issues/191)) ([a9f7f69](https://github.com/stevearc/oil.nvim/commit/a9f7f6927de2ceab01c9dfddd5a0d96330fe6374)) - - -### Bug Fixes - -* call vimL function in main loop ([#206](https://github.com/stevearc/oil.nvim/issues/206)) ([8418e94](https://github.com/stevearc/oil.nvim/commit/8418e94734e2572b422aead6e28d5a4c5b543d1f)) -* case handling for LSP willRenameFiles ([deba4db](https://github.com/stevearc/oil.nvim/commit/deba4db1aca6e3970c94499401da001694d01138)) -* disable swapfile for oil buffers ([#190](https://github.com/stevearc/oil.nvim/issues/190)) ([2e6996b](https://github.com/stevearc/oil.nvim/commit/2e6996b0757c454a8bbf1eb719d0b0b065442213)) -* more correct gf binding for ssh files ([1641357](https://github.com/stevearc/oil.nvim/commit/164135793d893efad9ed6f90ac74a1ab54c4182a)) -* parse errors when moving files across adapters ([4088efb](https://github.com/stevearc/oil.nvim/commit/4088efb8ff664b6f1624aab5dac6c3fe11d3962c)) -* path shortening does proper subpath detection ([054247b](https://github.com/stevearc/oil.nvim/commit/054247b9c1799edd5874231973db621553062a43)) -* restore original window when closing floating win ([#208](https://github.com/stevearc/oil.nvim/issues/208)) ([aea896a](https://github.com/stevearc/oil.nvim/commit/aea896a880e294c97a7c395dd8a6c89bdc93c644)) -* shorten path when opening files ([#194](https://github.com/stevearc/oil.nvim/issues/194), [#197](https://github.com/stevearc/oil.nvim/issues/197)) ([3275996](https://github.com/stevearc/oil.nvim/commit/3275996ce65f142d0e96b9fc2658f94e5bd43ad5)) -* shorten path when opening files ([#194](https://github.com/stevearc/oil.nvim/issues/194)) ([6cbc8d7](https://github.com/stevearc/oil.nvim/commit/6cbc8d725d3964cb08d679774db67d41fa002647)) - -## [2.2.0](https://github.com/stevearc/oil.nvim/compare/v2.1.0...v2.2.0) (2023-09-30) - - -### Features - -* action for opening entry in an external program ([#183](https://github.com/stevearc/oil.nvim/issues/183)) ([96a334a](https://github.com/stevearc/oil.nvim/commit/96a334abeb85a26af87585ec3810116c7cb7d172)) -* keymaps can specify mode ([#187](https://github.com/stevearc/oil.nvim/issues/187)) ([977da9a](https://github.com/stevearc/oil.nvim/commit/977da9ac6655b4f52bc26f23f584d9553f419555)) -* make gf work in ssh files ([#186](https://github.com/stevearc/oil.nvim/issues/186)) ([ee81363](https://github.com/stevearc/oil.nvim/commit/ee813638d2d042e4b8e6e8ffd00dae438bdbd4ca)) - - -### Bug Fixes - -* add busybox support for ssh adapter ([#173](https://github.com/stevearc/oil.nvim/issues/173)) ([a9ceb90](https://github.com/stevearc/oil.nvim/commit/a9ceb90a63955c409b6fbac0f5cfc4c2f43093fd)) -* correctly resolve new files when selected ([#179](https://github.com/stevearc/oil.nvim/issues/179)) ([83e4d04](https://github.com/stevearc/oil.nvim/commit/83e4d049228233df1870c92e160effb33e314396)) -* don't override FloatTitle highlight ([#189](https://github.com/stevearc/oil.nvim/issues/189)) ([5ced687](https://github.com/stevearc/oil.nvim/commit/5ced687ddd08e1f8df27a23884d516a9b24101fc)) -* hide swapfile error when editing file ([#188](https://github.com/stevearc/oil.nvim/issues/188)) ([bfc5a4c](https://github.com/stevearc/oil.nvim/commit/bfc5a4c48f4a53b95648e41d91e49b83fb03e919)) - -## [2.1.0](https://github.com/stevearc/oil.nvim/compare/v2.0.1...v2.1.0) (2023-09-11) - - -### Features - -* api to sort directory contents ([#169](https://github.com/stevearc/oil.nvim/issues/169)) ([879d280](https://github.com/stevearc/oil.nvim/commit/879d280617045d5a00d7a053e86d51c6c80970be)) - - -### Bug Fixes - -* allow converting a file to directory and vice-versa ([#117](https://github.com/stevearc/oil.nvim/issues/117)) ([926ae06](https://github.com/stevearc/oil.nvim/commit/926ae067eb9a79817a455d5ab2dc6f420beb53c0)) -* change default winblend for floating window to 0 ([#167](https://github.com/stevearc/oil.nvim/issues/167)) ([7033d52](https://github.com/stevearc/oil.nvim/commit/7033d52db012666b85504fe9a678939e49bc14b7)) -* lock cursor to first mutable column ([d4eb4f3](https://github.com/stevearc/oil.nvim/commit/d4eb4f3bbf7770d04070707c947655a5426d7f75)) - -## [2.0.1](https://github.com/stevearc/oil.nvim/compare/v2.0.0...v2.0.1) (2023-08-26) - - -### Bug Fixes - -* data loss bug when move + delete ([#162](https://github.com/stevearc/oil.nvim/issues/162)) ([f86d494](https://github.com/stevearc/oil.nvim/commit/f86d49446ae344ba3762d5705505aa09c1c1d4ee)) - -## [2.0.0](https://github.com/stevearc/oil.nvim/compare/v1.1.0...v2.0.0) (2023-08-24) - - -### ⚠ BREAKING CHANGES - -* disable netrw by default ([#155](https://github.com/stevearc/oil.nvim/issues/155)) - -### Bug Fixes - -* actions.terminal supports ssh adapter ([#152](https://github.com/stevearc/oil.nvim/issues/152)) ([0ccf95a](https://github.com/stevearc/oil.nvim/commit/0ccf95ae5d0ea731de8d427304f95d384a0664c4)) -* errors when writing files over ssh ([#159](https://github.com/stevearc/oil.nvim/issues/159)) ([bfa0e87](https://github.com/stevearc/oil.nvim/commit/bfa0e8705eb83a0724aed6d5dc9d21aa62a8986b)) -* fix flaky test ([9509ae0](https://github.com/stevearc/oil.nvim/commit/9509ae0feed5af04e4652375740a0722f2ee1a64)) -* remaining type errors ([8f78079](https://github.com/stevearc/oil.nvim/commit/8f7807946a67b5f1a515946f82251e33651bae29)) -* set nomodifiable after BufWritePre in ssh adapter ([#159](https://github.com/stevearc/oil.nvim/issues/159)) ([b61bc9b](https://github.com/stevearc/oil.nvim/commit/b61bc9b701a3cfb05cb6668446b0303cda7435e6)) -* sometimes use shell to run trash command ([#99](https://github.com/stevearc/oil.nvim/issues/99)) ([ff62fc2](https://github.com/stevearc/oil.nvim/commit/ff62fc28cd7976e49ddff6897a4f870785187f13)) -* ssh adapter supports any system with /bin/sh ([#161](https://github.com/stevearc/oil.nvim/issues/161)) ([ebcd720](https://github.com/stevearc/oil.nvim/commit/ebcd720a0987ed39f943c4a5d32b96d42e9cf695)) -* type annotations and type errors ([47c7737](https://github.com/stevearc/oil.nvim/commit/47c77376189e4063b4fcc6dc2c4cfe8ffd72c782)) - - -### Performance Improvements - -* tweak uv readdir params for performance ([ffb89bf](https://github.com/stevearc/oil.nvim/commit/ffb89bf416a4883cc12e5ed247885d4700b00a0f)) - - -### Code Refactoring - -* disable netrw by default ([#155](https://github.com/stevearc/oil.nvim/issues/155)) ([9d90893](https://github.com/stevearc/oil.nvim/commit/9d90893c377b6b75230e4bad177f8d0103ceafe4)) - ## [1.1.0](https://github.com/stevearc/oil.nvim/compare/v1.0.0...v1.1.0) (2023-08-09) diff --git a/Makefile b/Makefile deleted file mode 100644 index 10f01d1..0000000 --- a/Makefile +++ /dev/null @@ -1,66 +0,0 @@ -## help: print this help message -.PHONY: help -help: - @echo 'Usage:' - @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' - -## all: generate docs, lint, and run tests -.PHONY: all -all: doc lint test - -venv: - python3 -m venv venv - venv/bin/pip install -r scripts/requirements.txt - -## doc: generate documentation -.PHONY: doc -doc: scripts/nvim_doc_tools venv - venv/bin/python scripts/main.py generate - venv/bin/python scripts/main.py lint - -## test: run tests -.PHONY: test -test: - ./run_tests.sh - -## lint: run linters and LuaLS typechecking -.PHONY: lint -lint: scripts/nvim-typecheck-action fastlint - ./scripts/nvim-typecheck-action/typecheck.sh --workdir scripts/nvim-typecheck-action lua - -## fastlint: run only fast linters -.PHONY: fastlint -fastlint: scripts/nvim_doc_tools venv - venv/bin/python scripts/main.py lint - luacheck lua tests --formatter plain - stylua --check lua tests - -## profile: use LuaJIT profiler to profile the plugin -.PHONY: profile -profile: scripts/benchmark.nvim - nvim --clean -u perf/bootstrap.lua -c 'lua jit_profile()' - -## flame_profile: create a trace in the chrome profiler format -.PHONY: flame_profile -flame_profile: scripts/benchmark.nvim - nvim --clean -u perf/bootstrap.lua -c 'lua flame_profile()' - -## benchmark: benchmark performance opening directory with many files -.PHONY: benchmark -benchmark: scripts/benchmark.nvim - nvim --clean -u perf/bootstrap.lua -c 'lua benchmark()' - @cat perf/tmp/benchmark.txt - -scripts/nvim_doc_tools: - git clone https://github.com/stevearc/nvim_doc_tools scripts/nvim_doc_tools - -scripts/nvim-typecheck-action: - git clone https://github.com/stevearc/nvim-typecheck-action scripts/nvim-typecheck-action - -scripts/benchmark.nvim: - git clone https://github.com/stevearc/benchmark.nvim scripts/benchmark.nvim - -## clean: reset the repository to a clean state -.PHONY: clean -clean: - rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv perf/tmp profile.json diff --git a/README.md b/README.md index bf12bed..8a394c2 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,6 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94 - [Quick start](#quick-start) - [Options](#options) - [Adapters](#adapters) -- [Recipes](#recipes) -- [Third-party extensions](#third-party-extensions) - [API](#api) - [FAQ](#faq) @@ -21,9 +19,7 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94 ## Requirements - Neovim 0.8+ -- Icon provider plugin (optional) - - [mini.icons](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-icons.md) for file and folder icons - - [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) for file icons +- (optional) [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) for file icons ## Installation @@ -35,14 +31,9 @@ oil.nvim supports all the usual plugin managers ```lua { 'stevearc/oil.nvim', - ---@module 'oil' - ---@type oil.SetupOpts opts = {}, -- Optional dependencies - dependencies = { { "nvim-mini/mini.icons", opts = {} } }, - -- dependencies = { "nvim-tree/nvim-web-devicons" }, -- use if you prefer nvim-web-devicons - -- Lazy loading is not recommended because it is very tricky to make it work correctly in all situations. - lazy = false, + dependencies = { "nvim-tree/nvim-web-devicons" }, } ``` @@ -52,13 +43,11 @@ oil.nvim supports all the usual plugin managers Packer ```lua -require("packer").startup(function() - use({ - "stevearc/oil.nvim", - config = function() - require("oil").setup() - end, - }) +require('packer').startup(function() + use { + 'stevearc/oil.nvim', + config = function() require('oil').setup() end + } end) ``` @@ -68,9 +57,9 @@ end) Paq ```lua -require("paq")({ - { "stevearc/oil.nvim" }, -}) +require "paq" { + {'stevearc/oil.nvim'}; +} ``` @@ -125,7 +114,7 @@ Then open a directory with `nvim .`. Use `` to open a file/directory, and `- If you want to mimic the `vim-vinegar` method of navigating to the parent directory of a file, add this keymap: ```lua -vim.keymap.set("n", "-", "Oil", { desc = "Open parent directory" }) +vim.keymap.set("n", "-", require("oil").open, { desc = "Open parent directory" }) ``` You can open a directory with `:edit ` or `:Oil `. To open oil in a floating window, do `:Oil --float `. @@ -134,9 +123,6 @@ You can open a directory with `:edit ` or `:Oil `. To open oil in a ```lua require("oil").setup({ - -- Oil will take over directory buffers (e.g. `vim .` or `:e src/`) - -- Set to false if you want some other plugin (e.g. netrw) to open when you edit directories. - default_file_explorer = true, -- Id is automatically added at the beginning, and name at the end -- See :help oil-columns columns = { @@ -159,56 +145,40 @@ require("oil").setup({ spell = false, list = false, conceallevel = 3, - concealcursor = "nvic", + concealcursor = "n", }, - -- Send deleted files to the trash instead of permanently deleting them (:help oil-trash) - delete_to_trash = false, - -- Skip the confirmation popup for simple operations (:help oil.skip_confirm_for_simple_edits) + -- Oil will take over directory buffers (e.g. `vim .` or `:e src/` + default_file_explorer = true, + -- Restore window options to previous values when leaving an oil buffer + restore_win_options = true, + -- Skip the confirmation popup for simple operations skip_confirm_for_simple_edits = false, + -- Deleted files will be removed with the trash_command (below). + delete_to_trash = false, + -- Change this to customize the command used when deleting to trash + trash_command = "trash-put", -- Selecting a new/moved/renamed file or directory will prompt you to save changes first - -- (:help prompt_save_on_select_new_entry) prompt_save_on_select_new_entry = true, - -- Oil will automatically delete hidden buffers after this delay - -- You can set the delay to false to disable cleanup entirely - -- Note that the cleanup process only starts when none of the oil buffers are currently displayed - cleanup_delay_ms = 2000, - lsp_file_methods = { - -- Enable or disable LSP file operations - enabled = true, - -- Time to wait for LSP file operations to complete before skipping - timeout_ms = 1000, - -- Set to true to autosave buffers that are updated with LSP willRenameFiles - -- Set to "unmodified" to only save unmodified buffers - autosave_changes = false, - }, - -- Constrain the cursor to the editable parts of the oil buffer - -- Set to `false` to disable, or "name" to keep it on the file names - constrain_cursor = "editable", - -- Set to true to watch the filesystem for changes and reload oil - watch_for_changes = false, -- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap - -- options with a `callback` (e.g. { callback = function() ... end, desc = "", mode = "n" }) + -- options with a `callback` (e.g. { callback = function() ... end, desc = "", nowait = true }) -- Additionally, if it is a string that matches "actions.", -- it will use the mapping at require("oil.actions"). -- Set to `false` to remove a keymap -- See :help oil-actions for a list of all available actions keymaps = { - ["g?"] = { "actions.show_help", mode = "n" }, + ["g?"] = "actions.show_help", [""] = "actions.select", - [""] = { "actions.select", opts = { vertical = true } }, - [""] = { "actions.select", opts = { horizontal = true } }, - [""] = { "actions.select", opts = { tab = true } }, + [""] = "actions.select_vsplit", + [""] = "actions.select_split", + [""] = "actions.select_tab", [""] = "actions.preview", - [""] = { "actions.close", mode = "n" }, + [""] = "actions.close", [""] = "actions.refresh", - ["-"] = { "actions.parent", mode = "n" }, - ["_"] = { "actions.open_cwd", mode = "n" }, - ["`"] = { "actions.cd", mode = "n" }, - ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" }, - ["gs"] = { "actions.change_sort", mode = "n" }, - ["gx"] = "actions.open_external", - ["g."] = { "actions.toggle_hidden", mode = "n" }, - ["g\\"] = { "actions.toggle_trash", mode = "n" }, + ["-"] = "actions.parent", + ["_"] = "actions.open_cwd", + ["`"] = "actions.cd", + ["~"] = "actions.tcd", + ["g."] = "actions.toggle_hidden", }, -- Set to false to disable all of the above keymaps use_default_keymaps = true, @@ -217,82 +187,31 @@ require("oil").setup({ show_hidden = false, -- This function defines what is considered a "hidden" file is_hidden_file = function(name, bufnr) - local m = name:match("^%.") - return m ~= nil + return vim.startswith(name, ".") end, -- This function defines what will never be shown, even when `show_hidden` is set is_always_hidden = function(name, bufnr) return false end, - -- Sort file names with numbers in a more intuitive order for humans. - -- Can be "fast", true, or false. "fast" will turn it off for large directories. - natural_order = "fast", - -- Sort file and directory names case insensitive - case_insensitive = false, - sort = { - -- sort order can be "asc" or "desc" - -- see :help oil-columns to see which columns are sortable - { "type", "asc" }, - { "name", "asc" }, - }, - -- Customize the highlight group for the file name - highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan) - return nil - end, - }, - -- Extra arguments to pass to SCP when moving/copying files over SSH - extra_scp_args = {}, - -- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3 - extra_s3_args = {}, - -- EXPERIMENTAL support for performing file operations with git - git = { - -- Return true to automatically git add/mv/rm files - add = function(path) - return false - end, - mv = function(src_path, dest_path) - return false - end, - rm = function(path) - return false - end, }, -- Configuration for the floating window in oil.open_float float = { -- Padding around the floating window padding = 2, - -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) max_width = 0, max_height = 0, - border = nil, + border = "rounded", win_options = { - winblend = 0, + winblend = 10, }, - -- optionally override the oil buffers window title with custom function: fun(winid: integer): string - get_win_title = nil, - -- preview_split: Split direction: "auto", "left", "right", "above", "below". - preview_split = "auto", -- This is the config that will be passed to nvim_open_win. -- Change values here to customize the layout override = function(conf) return conf end, }, - -- Configuration for the file preview window - preview_win = { - -- Whether the preview window is automatically updated when the cursor is moved - update_on_cursor_moved = true, - -- How to open the preview window "load"|"scratch"|"fast_scratch" - preview_method = "fast_scratch", - -- A function that returns true to disable preview on a file e.g. to avoid lag - disable_preview = function(filename) - return false - end, - -- Window-local options to use for preview window buffers - win_options = {}, - }, - -- Configuration for the floating action confirmation window - confirmation = { + -- Configuration for the actions floating preview window + preview = { -- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- min_width and max_width can be a single value or a list of mixed integer/float types. -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total" @@ -309,7 +228,7 @@ require("oil").setup({ min_height = { 5, 0.1 }, -- optionally define an integer/float for the exact height of the preview window height = nil, - border = nil, + border = "rounded", win_options = { winblend = 0, }, @@ -322,20 +241,12 @@ require("oil").setup({ max_height = { 10, 0.9 }, min_height = { 5, 0.1 }, height = nil, - border = nil, + border = "rounded", minimized_border = "none", win_options = { winblend = 0, }, }, - -- Configuration for the floating SSH window - ssh = { - border = nil, - }, - -- Configuration for the floating keymaps help window - keymaps_help = { - border = nil, - }, }) ``` @@ -355,31 +266,7 @@ nvim oil-ssh://[username@]hostname[:port]/[path] This may look familiar. In fact, this is the same url format that netrw uses. -Note that at the moment the ssh adapter does not support Windows machines, and it requires the server to have a `/bin/sh` binary as well as standard unix commands (`ls`, `rm`, `mv`, `mkdir`, `chmod`, `cp`, `touch`, `ln`, `echo`). - -### S3 - -This adapter allows you to browse files stored in aws s3. To use it, make sure `aws` is setup correctly and then simply open a buffer using the following name template: - -``` -nvim oil-s3://[bucket]/[path] -``` - -Note that older versions of Neovim don't support numbers in the url, so for Neovim 0.11 and older the url starts with `oil-sss`. - -## Recipes - -- [Toggle file detail view](doc/recipes.md#toggle-file-detail-view) -- [Show CWD in the winbar](doc/recipes.md#show-cwd-in-the-winbar) -- [Hide gitignored files and show git tracked hidden files](doc/recipes.md#hide-gitignored-files-and-show-git-tracked-hidden-files) - -## Third-party extensions - -These are plugins maintained by other authors that extend the functionality of oil.nvim. - -- [oil-git-status.nvim](https://github.com/refractalize/oil-git-status.nvim) - Shows git status of files in statuscolumn -- [oil-git.nvim](https://github.com/benomahony/oil-git.nvim) - Shows git status of files with colour and symbols -- [oil-lsp-diagnostics.nvim](https://github.com/JezerM/oil-lsp-diagnostics.nvim) - Shows LSP diagnostics indicator as virtual text +Note that at the moment the ssh adapter does not support Windows machines, and it requires the server to have a `/bin/bash` binary as well as standard unix commands (`rm`, `mv`, `mkdir`, `chmod`, `cp`, `touch`, `ln`, `echo`). ## API @@ -389,17 +276,15 @@ These are plugins maintained by other authors that extend the functionality of o - [get_cursor_entry()](doc/api.md#get_cursor_entry) - [discard_all_changes()](doc/api.md#discard_all_changes) - [set_columns(cols)](doc/api.md#set_columnscols) -- [set_sort(sort)](doc/api.md#set_sortsort) - [set_is_hidden_file(is_hidden_file)](doc/api.md#set_is_hidden_fileis_hidden_file) - [toggle_hidden()](doc/api.md#toggle_hidden) -- [get_current_dir(bufnr)](doc/api.md#get_current_dirbufnr) -- [open_float(dir, opts, cb)](doc/api.md#open_floatdir-opts-cb) -- [toggle_float(dir, opts, cb)](doc/api.md#toggle_floatdir-opts-cb) -- [open(dir, opts, cb)](doc/api.md#opendir-opts-cb) -- [close(opts)](doc/api.md#closeopts) -- [open_preview(opts, callback)](doc/api.md#open_previewopts-callback) +- [get_current_dir()](doc/api.md#get_current_dir) +- [open_float(dir)](doc/api.md#open_floatdir) +- [toggle_float(dir)](doc/api.md#toggle_floatdir) +- [open(dir)](doc/api.md#opendir) +- [close()](doc/api.md#close) - [select(opts, callback)](doc/api.md#selectopts-callback) -- [save(opts, cb)](doc/api.md#saveopts-cb) +- [save(opts)](doc/api.md#saveopts) - [setup(opts)](doc/api.md#setupopts) @@ -421,7 +306,7 @@ Plus, I think it's pretty slick ;) - You like to use a netrw-like view to browse directories (as opposed to a file tree) - AND you want to be able to edit your filesystem like a buffer -- AND you want to perform cross-directory actions. AFAIK there is no other plugin that does this. (update: [mini.files](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-files.md) also offers this functionality) +- AND you want to perform cross-directory actions. AFAIK there is no other plugin that does this. (update: [mini.files](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-files.md) also offers this functionality) If you don't need those features specifically, check out the alternatives listed below @@ -437,7 +322,7 @@ If you don't need those features specifically, check out the alternatives listed **A:** -- [mini.files](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-files.md): A newer plugin that also supports cross-directory filesystem-as-buffer edits. It utilizes a unique column view. +- [mini.files](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-files.md): A newer plugin that also supports cross-directory filesystem-as-buffer edits. It utilizes a unique column view. - [vim-vinegar](https://github.com/tpope/vim-vinegar): The granddaddy. This made me fall in love with single-directory file browsing. I stopped using it when I encountered netrw bugs and performance issues. - [defx.nvim](https://github.com/Shougo/defx.nvim): What I switched to after vim-vinegar. Much more flexible and performant, but requires python and the API is a little hard to work with. - [dirbuf.nvim](https://github.com/elihunter173/dirbuf.nvim): The first plugin I encountered that let you edit the filesystem like a buffer. Never used it because it [can't do cross-directory edits](https://github.com/elihunter173/dirbuf.nvim/issues/7). @@ -446,3 +331,12 @@ If you don't need those features specifically, check out the alternatives listed - [vidir](https://github.com/trapd00r/vidir): Never personally used, but might be the first plugin to come up with the idea of editing a directory like a buffer. There's also file trees like [neo-tree](https://github.com/nvim-neo-tree/neo-tree.nvim) and [nvim-tree](https://github.com/nvim-tree/nvim-tree.lua), but they're really a different category entirely. + +**Q: I don't need netrw anymore. How can I disable it?** + +**A:** Oil can fully replace netrw for local and ssh file browsing/editing, but keep in mind that netrw also supports rsync, http, ftp, and dav. If you don't need these other features, you can disable netrw with the following: + +```lua +vim.g.loaded_netrw = 1 +vim.g.loaded_netrwPlugin = 1 +``` diff --git a/doc/api.md b/doc/api.md index 24d4a50..f87c34e 100644 --- a/doc/api.md +++ b/doc/api.md @@ -6,17 +6,15 @@ - [get_cursor_entry()](#get_cursor_entry) - [discard_all_changes()](#discard_all_changes) - [set_columns(cols)](#set_columnscols) -- [set_sort(sort)](#set_sortsort) - [set_is_hidden_file(is_hidden_file)](#set_is_hidden_fileis_hidden_file) - [toggle_hidden()](#toggle_hidden) -- [get_current_dir(bufnr)](#get_current_dirbufnr) -- [open_float(dir, opts, cb)](#open_floatdir-opts-cb) -- [toggle_float(dir, opts, cb)](#toggle_floatdir-opts-cb) -- [open(dir, opts, cb)](#opendir-opts-cb) -- [close(opts)](#closeopts) -- [open_preview(opts, callback)](#open_previewopts-callback) +- [get_current_dir()](#get_current_dir) +- [open_float(dir)](#open_floatdir) +- [toggle_float(dir)](#toggle_floatdir) +- [open(dir)](#opendir) +- [close()](#close) - [select(opts, callback)](#selectopts-callback) -- [save(opts, cb)](#saveopts-cb) +- [save(opts)](#saveopts) - [setup(opts)](#setupopts) @@ -54,28 +52,14 @@ Change the display columns for oil | ----- | ------------------ | ---- | | cols | `oil.ColumnSpec[]` | | -## set_sort(sort) - -`set_sort(sort)` \ -Change the sort order for oil - -| Param | Type | Desc | -| ----- | ---------------- | ------------------------------------------------------------------------------------- | -| sort | `oil.SortSpec[]` | List of columns plus direction. See :help oil-columns to see which ones are sortable. | - -**Examples:** -```lua -require("oil").set_sort({ { "type", "asc" }, { "size", "desc" } }) -``` - ## set_is_hidden_file(is_hidden_file) `set_is_hidden_file(is_hidden_file)` \ Change how oil determines if the file is hidden -| Param | Type | Desc | -| -------------- | ------------------------------------------------ | -------------------------------------------- | -| is_hidden_file | `fun(filename: string, bufnr: integer): boolean` | Return true if the file/dir should be hidden | +| Param | Type | Desc | +| -------------- | ----------------------------------------------------- | -------------------------------------------- | +| is_hidden_file | `fun(filename: string, bufnr: nil\|integer): boolean` | Return true if the file/dir should be hidden | ## toggle_hidden() @@ -83,123 +67,79 @@ Change how oil determines if the file is hidden Toggle hidden files and directories -## get_current_dir(bufnr) +## get_current_dir() -`get_current_dir(bufnr): nil|string` \ +`get_current_dir(): nil|string` \ Get the current directory -| Param | Type | Desc | -| ----- | -------------- | ---- | -| bufnr | `nil\|integer` | | -## open_float(dir, opts, cb) +## open_float(dir) -`open_float(dir, opts, cb)` \ +`open_float(dir)` \ Open oil browser in a floating window -| Param | Type | Desc | -| ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | -| opts | `nil\|oil.OpenOpts` | | -| >preview | `nil\|oil.OpenPreviewOpts` | When present, open the preview window after opening oil | -| >>vertical | `nil\|boolean` | Open the buffer in a vertical split | -| >>horizontal | `nil\|boolean` | Open the buffer in a horizontal split | -| >>split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| cb | `nil\|fun()` | Called after the oil buffer is ready | +| Param | Type | Desc | +| ----- | ------------- | ------------------------------------------------------------------------------------------- | +| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | -## toggle_float(dir, opts, cb) +## toggle_float(dir) -`toggle_float(dir, opts, cb)` \ +`toggle_float(dir)` \ Open oil browser in a floating window, or close it if open -| Param | Type | Desc | -| ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | -| opts | `nil\|oil.OpenOpts` | | -| >preview | `nil\|oil.OpenPreviewOpts` | When present, open the preview window after opening oil | -| >>vertical | `nil\|boolean` | Open the buffer in a vertical split | -| >>horizontal | `nil\|boolean` | Open the buffer in a horizontal split | -| >>split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| cb | `nil\|fun()` | Called after the oil buffer is ready | +| Param | Type | Desc | +| ----- | ------------- | ------------------------------------------------------------------------------------------- | +| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | -## open(dir, opts, cb) +## open(dir) -`open(dir, opts, cb)` \ +`open(dir)` \ Open oil browser for a directory -| Param | Type | Desc | -| ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | -| opts | `nil\|oil.OpenOpts` | | -| >preview | `nil\|oil.OpenPreviewOpts` | When present, open the preview window after opening oil | -| >>vertical | `nil\|boolean` | Open the buffer in a vertical split | -| >>horizontal | `nil\|boolean` | Open the buffer in a horizontal split | -| >>split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| cb | `nil\|fun()` | Called after the oil buffer is ready | +| Param | Type | Desc | +| ----- | ------------- | ------------------------------------------------------------------------------------------- | +| dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | -## close(opts) +## close() -`close(opts)` \ +`close()` \ Restore the buffer that was present when oil was opened -| Param | Type | Desc | -| ----------------- | -------------------- | --------------------------------------------------- | -| opts | `nil\|oil.CloseOpts` | | -| >exit_if_last_buf | `nil\|boolean` | Exit vim if this oil buffer is the last open buffer | - -## open_preview(opts, callback) - -`open_preview(opts, callback)` \ -Preview the entry under the cursor in a split - -| Param | Type | Desc | -| ----------- | ------------------------------------------------------- | ---------------------------------------------- | -| opts | `nil\|oil.OpenPreviewOpts` | | -| >vertical | `nil\|boolean` | Open the buffer in a vertical split | -| >horizontal | `nil\|boolean` | Open the buffer in a horizontal split | -| >split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| callback | `nil\|fun(err: nil\|string)` | Called once the preview window has been opened | ## select(opts, callback) `select(opts, callback)` \ Select the entry under the cursor -| Param | Type | Desc | -| ----------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| opts | `nil\|oil.SelectOpts` | | -| >vertical | `nil\|boolean` | Open the buffer in a vertical split | -| >horizontal | `nil\|boolean` | Open the buffer in a horizontal split | -| >split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| >tab | `nil\|boolean` | Open the buffer in a new tab | -| >close | `nil\|boolean` | Close the original oil buffer once selection is made | -| >handle_buffer_callback | `nil\|fun(buf_id: integer)` | If defined, all other buffer related options here would be ignored. This callback allows you to take over the process of opening the buffer yourself. | -| callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened | +| Param | Type | Desc | | +| -------- | ---------------------------- | -------------------------------------------------- | ---------------------------------------------------- | +| opts | `nil\|table` | | | +| | vertical | `boolean` | Open the buffer in a vertical split | +| | horizontal | `boolean` | Open the buffer in a horizontal split | +| | split | `"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | +| | preview | `boolean` | Open the buffer in a preview window | +| | tab | `boolean` | Open the buffer in a new tab | +| | close | `boolean` | Close the original oil buffer once selection is made | +| callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened | | -## save(opts, cb) +## save(opts) -`save(opts, cb)` \ +`save(opts)` \ Save all changes -| Param | Type | Desc | -| -------- | ---------------------------- | ------------------------------------------------------------------------------------------- | -| opts | `nil\|table` | | -| >confirm | `nil\|boolean` | Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil | -| cb | `nil\|fun(err: nil\|string)` | Called when mutations complete. | - -**Note:** -
-If you provide your own callback function, there will be no notification for errors.
-
+| Param | Type | Desc | | +| ----- | ------------ | -------------- | ------------------------------------------------------------------------------------------- | +| opts | `nil\|table` | | | +| | confirm | `nil\|boolean` | Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil | ## setup(opts) `setup(opts)` \ Initialize oil -| Param | Type | Desc | -| ----- | -------------------- | ---- | -| opts | `oil.setupOpts\|nil` | | +| Param | Type | Desc | +| ----- | ------------ | ---- | +| opts | `nil\|table` | | diff --git a/doc/oil.txt b/doc/oil.txt index 8753f87..bdca4d9 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -3,22 +3,17 @@ -------------------------------------------------------------------------------- CONTENTS *oil-contents* - 1. Config |oil-config| - 2. Options |oil-options| - 3. Api |oil-api| - 4. Columns |oil-columns| - 5. Actions |oil-actions| - 6. Highlights |oil-highlights| - 7. Trash |oil-trash| + 1. Options.....................................................|oil-options| + 2. Api.............................................................|oil-api| + 3. Columns.....................................................|oil-columns| + 4. Actions.....................................................|oil-actions| + 5. Highlights...............................................|oil-highlights| -------------------------------------------------------------------------------- -CONFIG *oil-config* +OPTIONS *oil-options* ->lua +> require("oil").setup({ - -- Oil will take over directory buffers (e.g. `vim .` or `:e src/`) - -- Set to false if you want some other plugin (e.g. netrw) to open when you edit directories. - default_file_explorer = true, -- Id is automatically added at the beginning, and name at the end -- See :help oil-columns columns = { @@ -41,56 +36,40 @@ CONFIG *oil-confi spell = false, list = false, conceallevel = 3, - concealcursor = "nvic", + concealcursor = "n", }, - -- Send deleted files to the trash instead of permanently deleting them (:help oil-trash) - delete_to_trash = false, - -- Skip the confirmation popup for simple operations (:help oil.skip_confirm_for_simple_edits) + -- Oil will take over directory buffers (e.g. `vim .` or `:e src/` + default_file_explorer = true, + -- Restore window options to previous values when leaving an oil buffer + restore_win_options = true, + -- Skip the confirmation popup for simple operations skip_confirm_for_simple_edits = false, + -- Deleted files will be removed with the trash_command (below). + delete_to_trash = false, + -- Change this to customize the command used when deleting to trash + trash_command = "trash-put", -- Selecting a new/moved/renamed file or directory will prompt you to save changes first - -- (:help prompt_save_on_select_new_entry) prompt_save_on_select_new_entry = true, - -- Oil will automatically delete hidden buffers after this delay - -- You can set the delay to false to disable cleanup entirely - -- Note that the cleanup process only starts when none of the oil buffers are currently displayed - cleanup_delay_ms = 2000, - lsp_file_methods = { - -- Enable or disable LSP file operations - enabled = true, - -- Time to wait for LSP file operations to complete before skipping - timeout_ms = 1000, - -- Set to true to autosave buffers that are updated with LSP willRenameFiles - -- Set to "unmodified" to only save unmodified buffers - autosave_changes = false, - }, - -- Constrain the cursor to the editable parts of the oil buffer - -- Set to `false` to disable, or "name" to keep it on the file names - constrain_cursor = "editable", - -- Set to true to watch the filesystem for changes and reload oil - watch_for_changes = false, -- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap - -- options with a `callback` (e.g. { callback = function() ... end, desc = "", mode = "n" }) + -- options with a `callback` (e.g. { callback = function() ... end, desc = "", nowait = true }) -- Additionally, if it is a string that matches "actions.", -- it will use the mapping at require("oil.actions"). -- Set to `false` to remove a keymap -- See :help oil-actions for a list of all available actions keymaps = { - ["g?"] = { "actions.show_help", mode = "n" }, + ["g?"] = "actions.show_help", [""] = "actions.select", - [""] = { "actions.select", opts = { vertical = true } }, - [""] = { "actions.select", opts = { horizontal = true } }, - [""] = { "actions.select", opts = { tab = true } }, + [""] = "actions.select_vsplit", + [""] = "actions.select_split", + [""] = "actions.select_tab", [""] = "actions.preview", - [""] = { "actions.close", mode = "n" }, + [""] = "actions.close", [""] = "actions.refresh", - ["-"] = { "actions.parent", mode = "n" }, - ["_"] = { "actions.open_cwd", mode = "n" }, - ["`"] = { "actions.cd", mode = "n" }, - ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" }, - ["gs"] = { "actions.change_sort", mode = "n" }, - ["gx"] = "actions.open_external", - ["g."] = { "actions.toggle_hidden", mode = "n" }, - ["g\\"] = { "actions.toggle_trash", mode = "n" }, + ["-"] = "actions.parent", + ["_"] = "actions.open_cwd", + ["`"] = "actions.cd", + ["~"] = "actions.tcd", + ["g."] = "actions.toggle_hidden", }, -- Set to false to disable all of the above keymaps use_default_keymaps = true, @@ -99,82 +78,31 @@ CONFIG *oil-confi show_hidden = false, -- This function defines what is considered a "hidden" file is_hidden_file = function(name, bufnr) - local m = name:match("^%.") - return m ~= nil + return vim.startswith(name, ".") end, -- This function defines what will never be shown, even when `show_hidden` is set is_always_hidden = function(name, bufnr) return false end, - -- Sort file names with numbers in a more intuitive order for humans. - -- Can be "fast", true, or false. "fast" will turn it off for large directories. - natural_order = "fast", - -- Sort file and directory names case insensitive - case_insensitive = false, - sort = { - -- sort order can be "asc" or "desc" - -- see :help oil-columns to see which columns are sortable - { "type", "asc" }, - { "name", "asc" }, - }, - -- Customize the highlight group for the file name - highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan) - return nil - end, - }, - -- Extra arguments to pass to SCP when moving/copying files over SSH - extra_scp_args = {}, - -- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3 - extra_s3_args = {}, - -- EXPERIMENTAL support for performing file operations with git - git = { - -- Return true to automatically git add/mv/rm files - add = function(path) - return false - end, - mv = function(src_path, dest_path) - return false - end, - rm = function(path) - return false - end, }, -- Configuration for the floating window in oil.open_float float = { -- Padding around the floating window padding = 2, - -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) max_width = 0, max_height = 0, - border = nil, + border = "rounded", win_options = { - winblend = 0, + winblend = 10, }, - -- optionally override the oil buffers window title with custom function: fun(winid: integer): string - get_win_title = nil, - -- preview_split: Split direction: "auto", "left", "right", "above", "below". - preview_split = "auto", -- This is the config that will be passed to nvim_open_win. -- Change values here to customize the layout override = function(conf) return conf end, }, - -- Configuration for the file preview window - preview_win = { - -- Whether the preview window is automatically updated when the cursor is moved - update_on_cursor_moved = true, - -- How to open the preview window "load"|"scratch"|"fast_scratch" - preview_method = "fast_scratch", - -- A function that returns true to disable preview on a file e.g. to avoid lag - disable_preview = function(filename) - return false - end, - -- Window-local options to use for preview window buffers - win_options = {}, - }, - -- Configuration for the floating action confirmation window - confirmation = { + -- Configuration for the actions floating preview window + preview = { -- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- min_width and max_width can be a single value or a list of mixed integer/float types. -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total" @@ -191,7 +119,7 @@ CONFIG *oil-confi min_height = { 5, 0.1 }, -- optionally define an integer/float for the exact height of the preview window height = nil, - border = nil, + border = "rounded", win_options = { winblend = 0, }, @@ -204,56 +132,15 @@ CONFIG *oil-confi max_height = { 10, 0.9 }, min_height = { 5, 0.1 }, height = nil, - border = nil, + border = "rounded", minimized_border = "none", win_options = { winblend = 0, }, }, - -- Configuration for the floating SSH window - ssh = { - border = nil, - }, - -- Configuration for the floating keymaps help window - keymaps_help = { - border = nil, - }, }) < --------------------------------------------------------------------------------- -OPTIONS *oil-options* - - -skip_confirm_for_simple_edits *oil.skip_confirm_for_simple_edits* - type: `boolean` default: `false` - Before performing filesystem operations, Oil displays a confirmation popup to ensure - that all operations are intentional. When this option is `true`, the popup will be - skipped if the operations: - * contain no deletes - * contain no cross-adapter moves or copies (e.g. from local to ssh) - * contain at most one copy or move - * contain at most five creates - -prompt_save_on_select_new_entry *oil.prompt_save_on_select_new_entry* - type: `boolean` default: `true` - There are two cases where this option is relevant: - 1. You copy a file to a new location, then you select it and make edits before - saving. - 2. You copy a directory to a new location, then you enter the directory and make - changes before saving. - - In case 1, when you edit the file you are actually editing the original file because - oil has not yet moved/copied it to its new location. This means that the original - file will, perhaps unexpectedly, also be changed by any edits you make. - - Case 2 is similar; when you edit the directory you are again actually editing the - original location of the directory. If you add new files, those files will be - created in both the original location and the copied directory. - - When this option is `true`, Oil will prompt you to save before entering a file or - directory that is pending within oil, but does not exist on disk. - -------------------------------------------------------------------------------- API *oil-api* @@ -278,135 +165,75 @@ set_columns({cols}) *oil.set_column Parameters: {cols} `oil.ColumnSpec[]` -set_sort({sort}) *oil.set_sort* - Change the sort order for oil - - Parameters: - {sort} `oil.SortSpec[]` List of columns plus direction. See :help oil- - columns to see which ones are sortable. - - Examples: >lua - require("oil").set_sort({ { "type", "asc" }, { "size", "desc" } }) -< - set_is_hidden_file({is_hidden_file}) *oil.set_is_hidden_file* Change how oil determines if the file is hidden Parameters: - {is_hidden_file} `fun(filename: string, bufnr: integer): boolean` Return - true if the file/dir should be hidden + {is_hidden_file} `fun(filename: string, bufnr: nil|integer): boolean` Retu + rn true if the file/dir should be hidden toggle_hidden() *oil.toggle_hidden* Toggle hidden files and directories -get_current_dir({bufnr}): nil|string *oil.get_current_dir* +get_current_dir(): nil|string *oil.get_current_dir* Get the current directory - Parameters: - {bufnr} `nil|integer` -open_float({dir}, {opts}, {cb}) *oil.open_float* +open_float({dir}) *oil.open_float* Open oil browser in a floating window Parameters: - {dir} `nil|string` When nil, open the parent of the current buffer, or - the cwd if current buffer is not a file - {opts} `nil|oil.OpenOpts` - {preview} `nil|oil.OpenPreviewOpts` When present, open the preview - window after opening oil - {vertical} `nil|boolean` Open the buffer in a vertical split - {horizontal} `nil|boolean` Open the buffer in a horizontal split - {split} `nil|"aboveleft"|"belowright"|"topleft"|"botright"` S - plit modifier - {cb} `nil|fun()` Called after the oil buffer is ready + {dir} `nil|string` When nil, open the parent of the current buffer, or the + cwd if current buffer is not a file -toggle_float({dir}, {opts}, {cb}) *oil.toggle_float* +toggle_float({dir}) *oil.toggle_float* Open oil browser in a floating window, or close it if open Parameters: - {dir} `nil|string` When nil, open the parent of the current buffer, or - the cwd if current buffer is not a file - {opts} `nil|oil.OpenOpts` - {preview} `nil|oil.OpenPreviewOpts` When present, open the preview - window after opening oil - {vertical} `nil|boolean` Open the buffer in a vertical split - {horizontal} `nil|boolean` Open the buffer in a horizontal split - {split} `nil|"aboveleft"|"belowright"|"topleft"|"botright"` S - plit modifier - {cb} `nil|fun()` Called after the oil buffer is ready + {dir} `nil|string` When nil, open the parent of the current buffer, or the + cwd if current buffer is not a file -open({dir}, {opts}, {cb}) *oil.open* +open({dir}) *oil.open* Open oil browser for a directory Parameters: - {dir} `nil|string` When nil, open the parent of the current buffer, or - the cwd if current buffer is not a file - {opts} `nil|oil.OpenOpts` - {preview} `nil|oil.OpenPreviewOpts` When present, open the preview - window after opening oil - {vertical} `nil|boolean` Open the buffer in a vertical split - {horizontal} `nil|boolean` Open the buffer in a horizontal split - {split} `nil|"aboveleft"|"belowright"|"topleft"|"botright"` S - plit modifier - {cb} `nil|fun()` Called after the oil buffer is ready + {dir} `nil|string` When nil, open the parent of the current buffer, or the + cwd if current buffer is not a file -close({opts}) *oil.close* +close() *oil.close* Restore the buffer that was present when oil was opened - Parameters: - {opts} `nil|oil.CloseOpts` - {exit_if_last_buf} `nil|boolean` Exit vim if this oil buffer is the - last open buffer - -open_preview({opts}, {callback}) *oil.open_preview* - Preview the entry under the cursor in a split - - Parameters: - {opts} `nil|oil.OpenPreviewOpts` - {vertical} `nil|boolean` Open the buffer in a vertical split - {horizontal} `nil|boolean` Open the buffer in a horizontal split - {split} `nil|"aboveleft"|"belowright"|"topleft"|"botright"` Split - modifier - {callback} `nil|fun(err: nil|string)` Called once the preview window has - been opened select({opts}, {callback}) *oil.select* Select the entry under the cursor Parameters: - {opts} `nil|oil.SelectOpts` - {vertical} `nil|boolean` Open the buffer in a vertical split - {horizontal} `nil|boolean` Open the buffer in a horizontal split - {split} `nil|"aboveleft"|"belowright"|"topleft"|"botright"` Split + {opts} `nil|table` + {vertical} `boolean` Open the buffer in a vertical split + {horizontal} `boolean` Open the buffer in a horizontal split + {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split modifier - {tab} `nil|boolean` Open the buffer in a new tab - {close} `nil|boolean` Close the original oil buffer once - selection is made - {handle_buffer_callback} `nil|fun(buf_id: integer)` If defined, all - other buffer related options here would be ignored. This - callback allows you to take over the process of opening - the buffer yourself. + {preview} `boolean` Open the buffer in a preview window + {tab} `boolean` Open the buffer in a new tab + {close} `boolean` Close the original oil buffer once selection is + made {callback} `nil|fun(err: nil|string)` Called once all entries have been opened -save({opts}, {cb}) *oil.save* +save({opts}) *oil.save* Save all changes Parameters: {opts} `nil|table` {confirm} `nil|boolean` Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil - {cb} `nil|fun(err: nil|string)` Called when mutations complete. - - Note: - If you provide your own callback function, there will be no notification for errors. setup({opts}) *oil.setup* Initialize oil Parameters: - {opts} `oil.setupOpts|nil` + {opts} `nil|table` -------------------------------------------------------------------------------- COLUMNS *oil-columns* @@ -416,13 +243,11 @@ or as a table to pass parameters (e.g. `{"size", highlight = "Special"}`) type *column-type* Adapters: * - Sortable: this column can be used in view_props.sort The type of the entry (file, directory, link, etc) Parameters: {highlight} `string|fun(value: string): string` Highlight group, or function that returns a highlight group - {align} `"left"|"center"|"right"` Text alignment within the column {icons} `table` Mapping of entry type to icon icon *column-icon* @@ -432,7 +257,6 @@ icon *column-ico Parameters: {highlight} `string|fun(value: string): string` Highlight group, or function that returns a highlight group - {align} `"left"|"center"|"right"` Text alignment within the column {default_file} `string` Fallback icon for files when nvim-web-devicons returns nil {directory} `string` Icon for directories @@ -440,14 +264,12 @@ icon *column-ico the icon size *column-size* - Adapters: files, ssh, s3 - Sortable: this column can be used in view_props.sort + Adapters: files, ssh The size of the file Parameters: {highlight} `string|fun(value: string): string` Highlight group, or function that returns a highlight group - {align} `"left"|"center"|"right"` Text alignment within the column permissions *column-permissions* Adapters: files, ssh @@ -457,228 +279,111 @@ permissions *column-permission Parameters: {highlight} `string|fun(value: string): string` Highlight group, or function that returns a highlight group - {align} `"left"|"center"|"right"` Text alignment within the column ctime *column-ctime* Adapters: files - Sortable: this column can be used in view_props.sort Change timestamp of the file Parameters: {highlight} `string|fun(value: string): string` Highlight group, or function that returns a highlight group - {align} `"left"|"center"|"right"` Text alignment within the column {format} `string` Format string (see :help strftime) mtime *column-mtime* Adapters: files - Sortable: this column can be used in view_props.sort Last modified time of the file Parameters: {highlight} `string|fun(value: string): string` Highlight group, or function that returns a highlight group - {align} `"left"|"center"|"right"` Text alignment within the column {format} `string` Format string (see :help strftime) atime *column-atime* Adapters: files - Sortable: this column can be used in view_props.sort Last access time of the file Parameters: {highlight} `string|fun(value: string): string` Highlight group, or function that returns a highlight group - {align} `"left"|"center"|"right"` Text alignment within the column {format} `string` Format string (see :help strftime) birthtime *column-birthtime* - Adapters: files, s3 - Sortable: this column can be used in view_props.sort + Adapters: files The time the file was created Parameters: {highlight} `string|fun(value: string): string` Highlight group, or function that returns a highlight group - {align} `"left"|"center"|"right"` Text alignment within the column {format} `string` Format string (see :help strftime) -------------------------------------------------------------------------------- ACTIONS *oil-actions* -The `keymaps` option in `oil.setup` allow you to create mappings using all the same parameters as |vim.keymap.set|. ->lua - keymaps = { - -- Mappings can be a string - ["~"] = "edit $HOME", - -- Mappings can be a function - ["gd"] = function() - require("oil").set_columns({ "icon", "permissions", "size", "mtime" }) - end, - -- You can pass additional opts to vim.keymap.set by using - -- a table with the mapping as the first element. - ["ff"] = { - function() - require("telescope.builtin").find_files({ - cwd = require("oil").get_current_dir() - }) - end, - mode = "n", - nowait = true, - desc = "Find files in the current directory" - }, - -- Mappings that are a string starting with "actions." will be - -- one of the built-in actions, documented below. - ["`"] = "actions.tcd", - -- Some actions have parameters. These are passed in via the `opts` key. - [":"] = { - "actions.open_cmdline", - opts = { - shorten_path = true, - modify = ":h", - }, - desc = "Open the command line with the current directory as an argument", - }, - } - -Below are the actions that can be used in the `keymaps` section of config -options. You can refer to them as strings (e.g. "actions.") or you -can use the functions directly with -`require("oil.actions").action_name.callback()` +These are actions that can be used in the `keymaps` section of config options. cd *actions.cd* :cd to the current oil directory - Parameters: - {scope} `nil|"tab"|"win"` Scope of the directory change (e.g. use |:tcd| - or |:lcd|) - {silent} `boolean` Do not show a message when changing directories - -change_sort *actions.change_sort* - Change the sort order - - Parameters: - {sort} `oil.SortSpec[]` List of columns plus direction (see - |oil.set_sort|) instead of interactive selection - close *actions.close* Close oil and restore original buffer - Parameters: - {exit_if_last_buf} `boolean` Exit vim if oil is closed as the last buffer - -copy_to_system_clipboard *actions.copy_to_system_clipboard* - Copy the entry under the cursor to the system clipboard +copy_entry_path *actions.copy_entry_path* + Yank the filepath of the entry under the cursor to a register open_cmdline *actions.open_cmdline* Open vim cmdline with current entry as an argument - Parameters: - {modify} `string` Modify the path with |fnamemodify()| using this as - the mods argument - {shorten_path} `boolean` Use relative paths when possible +open_cmdline_dir *actions.open_cmdline_dir* + Open vim cmdline with current directory as an argument open_cwd *actions.open_cwd* Open oil in Neovim's current working directory -open_external *actions.open_external* - Open the entry under the cursor in an external program - open_terminal *actions.open_terminal* Open a terminal in the current directory parent *actions.parent* Navigate to the parent path -paste_from_system_clipboard *actions.paste_from_system_clipboard* - Paste the system clipboard into the current oil directory - - Parameters: - {delete_original} `boolean` Delete the original file after copying - preview *actions.preview* Open the entry under the cursor in a preview window, or close the preview window if already open - Parameters: - {horizontal} `boolean` Open the buffer in a horizontal split - {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split - modifier - {vertical} `boolean` Open the buffer in a vertical split - preview_scroll_down *actions.preview_scroll_down* Scroll down in the preview window -preview_scroll_left *actions.preview_scroll_left* - Scroll left in the preview window - -preview_scroll_right *actions.preview_scroll_right* - Scroll right in the preview window - preview_scroll_up *actions.preview_scroll_up* Scroll up in the preview window refresh *actions.refresh* Refresh current directory list - Parameters: - {force} `boolean` When true, do not prompt user if they will be discarding - changes - select *actions.select* Open the entry under the cursor - Parameters: - {close} `boolean` Close the original oil buffer once selection is - made - {horizontal} `boolean` Open the buffer in a horizontal split - {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split - modifier - {tab} `boolean` Open the buffer in a new tab - {vertical} `boolean` Open the buffer in a vertical split +select_split *actions.select_split* + Open the entry under the cursor in a horizontal split -send_to_qflist *actions.send_to_qflist* - Sends files in the current oil directory to the quickfix list, replacing the - previous entries. +select_tab *actions.select_tab* + Open the entry under the cursor in a new tab - Parameters: - {action} `"r"|"a"` Replace or add to current quickfix list (see - |setqflist-action|) - {only_matching_search} `boolean` Whether to only add the files that - matches the last search. This option only applies when search - highlighting is active - {target} `"qflist"|"loclist"` The target list to send files to +select_vsplit *actions.select_vsplit* + Open the entry under the cursor in a vertical split show_help *actions.show_help* Show default keymaps +tcd *actions.tcd* + :tcd to the current oil directory + toggle_hidden *actions.toggle_hidden* Toggle hidden files and directories -toggle_trash *actions.toggle_trash* - Jump to and from the trash for the current directory - -yank_entry *actions.yank_entry* - Yank the filepath of the entry under the cursor to a register - - Parameters: - {modify} `string` Modify the path with |fnamemodify()| using this as the - mods argument - -------------------------------------------------------------------------------- HIGHLIGHTS *oil-highlights* -OilEmpty *hl-OilEmpty* - Empty column values - -OilHidden *hl-OilHidden* - Hidden entry in an oil buffer - OilDir *hl-OilDir* - Directory names in an oil buffer - -OilDirHidden *hl-OilDirHidden* - Hidden directory names in an oil buffer + Directories in an oil buffer OilDirIcon *hl-OilDirIcon* Icon for directories @@ -686,39 +391,12 @@ OilDirIcon *hl-OilDirIco OilSocket *hl-OilSocket* Socket files in an oil buffer -OilSocketHidden *hl-OilSocketHidden* - Hidden socket files in an oil buffer - OilLink *hl-OilLink* Soft links in an oil buffer -OilOrphanLink *hl-OilOrphanLink* - Orphaned soft links in an oil buffer - -OilLinkHidden *hl-OilLinkHidden* - Hidden soft links in an oil buffer - -OilOrphanLinkHidden *hl-OilOrphanLinkHidden* - Hidden orphaned soft links in an oil buffer - -OilLinkTarget *hl-OilLinkTarget* - The target of a soft link - -OilOrphanLinkTarget *hl-OilOrphanLinkTarget* - The target of an orphaned soft link - -OilLinkTargetHidden *hl-OilLinkTargetHidden* - The target of a hidden soft link - -OilOrphanLinkTargetHidden *hl-OilOrphanLinkTargetHidden* - The target of an hidden orphaned soft link - OilFile *hl-OilFile* Normal files in an oil buffer -OilFileHidden *hl-OilFileHidden* - Hidden normal files in an oil buffer - OilCreate *hl-OilCreate* Create action in the oil preview window @@ -734,45 +412,5 @@ OilCopy *hl-OilCop OilChange *hl-OilChange* Change action in the oil preview window -OilRestore *hl-OilRestore* - Restore (from the trash) action in the oil preview window - -OilPurge *hl-OilPurge* - Purge (Permanently delete a file from trash) action in the oil preview - window - -OilTrash *hl-OilTrash* - Trash (delete a file to trash) action in the oil preview window - -OilTrashSourcePath *hl-OilTrashSourcePath* - Virtual text that shows the original path of file in the trash - --------------------------------------------------------------------------------- -TRASH *oil-trash* - - -Oil has built-in support for using the system trash. When -`delete_to_trash = true`, any deleted files will be sent to the trash instead -of being permanently deleted. You can browse the trash for a directory using -the `toggle_trash` action (bound to `g\` by default). You can view all files -in the trash with `:Oil --trash /`. - -To restore files, simply move them from the trash to the desired destination, -the same as any other file operation. If you delete files from the trash they -will be permanently deleted (purged). - -Linux: - Oil supports the FreeDesktop trash specification. - https://specifications.freedesktop.org/trash-spec/1.0/ - All features should work. - -Mac: - Oil has limited support for MacOS due to the proprietary nature of the - implementation. The trash bin can only be viewed as a single dir - (instead of being able to see files that were trashed from a directory). - -Windows: - Oil supports the Windows Recycle Bin. All features should work. - ================================================================================ vim:tw=80:ts=2:ft=help:norl:syntax=help: diff --git a/doc/recipes.md b/doc/recipes.md deleted file mode 100644 index 0a19598..0000000 --- a/doc/recipes.md +++ /dev/null @@ -1,127 +0,0 @@ -# Recipes - -Have a cool recipe to share? Open a pull request and add it to this doc! - - - -- [Toggle file detail view](#toggle-file-detail-view) -- [Show CWD in the winbar](#show-cwd-in-the-winbar) -- [Hide gitignored files and show git tracked hidden files](#hide-gitignored-files-and-show-git-tracked-hidden-files) - - - -## Toggle file detail view - -```lua -local detail = false -require("oil").setup({ - keymaps = { - ["gd"] = { - desc = "Toggle file detail view", - callback = function() - detail = not detail - if detail then - require("oil").set_columns({ "icon", "permissions", "size", "mtime" }) - else - require("oil").set_columns({ "icon" }) - end - end, - }, - }, -}) -``` - -## Show CWD in the winbar - -```lua --- Declare a global function to retrieve the current directory -function _G.get_oil_winbar() - local bufnr = vim.api.nvim_win_get_buf(vim.g.statusline_winid) - local dir = require("oil").get_current_dir(bufnr) - if dir then - return vim.fn.fnamemodify(dir, ":~") - else - -- If there is no current directory (e.g. over ssh), just show the buffer name - return vim.api.nvim_buf_get_name(0) - end -end - -require("oil").setup({ - win_options = { - winbar = "%!v:lua.get_oil_winbar()", - }, -}) -``` - -## Hide gitignored files and show git tracked hidden files - -```lua --- helper function to parse output -local function parse_output(proc) - local result = proc:wait() - local ret = {} - if result.code == 0 then - for line in vim.gsplit(result.stdout, "\n", { plain = true, trimempty = true }) do - -- Remove trailing slash - line = line:gsub("/$", "") - ret[line] = true - end - end - return ret -end - --- build git status cache -local function new_git_status() - return setmetatable({}, { - __index = function(self, key) - local ignore_proc = vim.system( - { "git", "ls-files", "--ignored", "--exclude-standard", "--others", "--directory" }, - { - cwd = key, - text = true, - } - ) - local tracked_proc = vim.system({ "git", "ls-tree", "HEAD", "--name-only" }, { - cwd = key, - text = true, - }) - local ret = { - ignored = parse_output(ignore_proc), - tracked = parse_output(tracked_proc), - } - - rawset(self, key, ret) - return ret - end, - }) -end -local git_status = new_git_status() - --- Clear git status cache on refresh -local refresh = require("oil.actions").refresh -local orig_refresh = refresh.callback -refresh.callback = function(...) - git_status = new_git_status() - orig_refresh(...) -end - -require("oil").setup({ - view_options = { - is_hidden_file = function(name, bufnr) - local dir = require("oil").get_current_dir(bufnr) - local is_dotfile = vim.startswith(name, ".") and name ~= ".." - -- if no local directory (e.g. for ssh connections), just hide dotfiles - if not dir then - return is_dotfile - end - -- dotfiles are considered hidden unless tracked - if is_dotfile then - return not git_status[dir].tracked[name] - else - -- Check if file is gitignored - return git_status[dir].ignored[name] - end - end, - }, -}) -``` diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua index 1c61999..1da95f4 100644 --- a/lua/oil/actions.lua +++ b/lua/oil/actions.lua @@ -4,48 +4,20 @@ local util = require("oil.util") local M = {} M.show_help = { + desc = "Show default keymaps", callback = function() local config = require("oil.config") require("oil.keymap_util").show_help(config.keymaps) end, - desc = "Show default keymaps", } M.select = { desc = "Open the entry under the cursor", - callback = function(opts) - opts = opts or {} - local callback = opts.callback - opts.callback = nil - oil.select(opts, callback) - end, - parameters = { - vertical = { - type = "boolean", - desc = "Open the buffer in a vertical split", - }, - horizontal = { - type = "boolean", - desc = "Open the buffer in a horizontal split", - }, - split = { - type = '"aboveleft"|"belowright"|"topleft"|"botright"', - desc = "Split modifier", - }, - tab = { - type = "boolean", - desc = "Open the buffer in a new tab", - }, - close = { - type = "boolean", - desc = "Close the original oil buffer once selection is made", - }, - }, + callback = oil.select, } M.select_vsplit = { desc = "Open the entry under the cursor in a vertical split", - deprecated = true, callback = function() oil.select({ vertical = true }) end, @@ -53,7 +25,6 @@ M.select_vsplit = { M.select_split = { desc = "Open the entry under the cursor in a horizontal split", - deprecated = true, callback = function() oil.select({ horizontal = true }) end, @@ -61,7 +32,6 @@ M.select_split = { M.select_tab = { desc = "Open the entry under the cursor in a new tab", - deprecated = true, callback = function() oil.select({ tab = true }) end, @@ -69,21 +39,7 @@ M.select_tab = { M.preview = { desc = "Open the entry under the cursor in a preview window, or close the preview window if already open", - parameters = { - vertical = { - type = "boolean", - desc = "Open the buffer in a vertical split", - }, - horizontal = { - type = "boolean", - desc = "Open the buffer in a horizontal split", - }, - split = { - type = '"aboveleft"|"belowright"|"topleft"|"botright"', - desc = "Split modifier", - }, - }, - callback = function(opts) + callback = function() local entry = oil.get_cursor_entry() if not entry then vim.notify("Could not find entry under cursor", vim.log.levels.ERROR) @@ -94,15 +50,10 @@ M.preview = { local cur_id = vim.w[winid].oil_entry_id if entry.id == cur_id then vim.api.nvim_win_close(winid, true) - if util.is_floating_win() then - local layout = require("oil.layout") - local win_opts = layout.get_fullscreen_win_opts() - vim.api.nvim_win_set_config(0, win_opts) - end return end end - oil.open_preview(opts) + oil.select({ preview = true }) end, } @@ -136,30 +87,6 @@ M.preview_scroll_up = { end, } -M.preview_scroll_left = { - desc = "Scroll left in the preview window", - callback = function() - local winid = util.get_preview_win() - if winid then - vim.api.nvim_win_call(winid, function() - vim.cmd.normal({ "zH", bang = true }) - end) - end - end, -} - -M.preview_scroll_right = { - desc = "Scroll right in the preview window", - callback = function() - local winid = util.get_preview_win() - if winid then - vim.api.nvim_win_call(winid, function() - vim.cmd.normal({ "zL", bang = true }) - end) - end - end, -} - M.parent = { desc = "Navigate to the parent path", callback = oil.open, @@ -167,27 +94,14 @@ M.parent = { M.close = { desc = "Close oil and restore original buffer", - callback = function(opts) - opts = opts or {} - oil.close(opts) - end, - parameters = { - exit_if_last_buf = { - type = "boolean", - desc = "Exit vim if oil is closed as the last buffer", - }, - }, + callback = oil.close, } ---@param cmd string ----@param silent? boolean -local function cd(cmd, silent) +local function cd(cmd) local dir = oil.get_current_dir() if dir then vim.cmd({ cmd = cmd, args = { dir } }) - if not silent then - vim.notify(string.format("CWD: %s", dir), vim.log.levels.INFO) - end else vim.notify("Cannot :cd; not in a directory", vim.log.levels.WARN) end @@ -195,31 +109,13 @@ end M.cd = { desc = ":cd to the current oil directory", - callback = function(opts) - opts = opts or {} - local cmd = "cd" - if opts.scope == "tab" then - cmd = "tcd" - elseif opts.scope == "win" then - cmd = "lcd" - end - cd(cmd, opts.silent) + callback = function() + cd("cd") end, - parameters = { - scope = { - type = 'nil|"tab"|"win"', - desc = "Scope of the directory change (e.g. use |:tcd| or |:lcd|)", - }, - silent = { - type = "boolean", - desc = "Do not show a message when changing directories", - }, - }, } M.tcd = { desc = ":tcd to the current oil directory", - deprecated = true, callback = function() cd("tcd") end, @@ -242,129 +138,41 @@ M.toggle_hidden = { M.open_terminal = { desc = "Open a terminal in the current directory", callback = function() - local config = require("oil.config") - local bufname = vim.api.nvim_buf_get_name(0) - local adapter = config.get_adapter_by_scheme(bufname) - if not adapter then - return - end - if adapter.name == "files" then - local dir = oil.get_current_dir() - assert(dir, "Oil buffer with files adapter must have current directory") - local bufnr = vim.api.nvim_create_buf(false, true) - vim.api.nvim_set_current_buf(bufnr) - if vim.fn.has("nvim-0.11") == 1 then - vim.fn.jobstart(vim.o.shell, { cwd = dir, term = true }) - else - ---@diagnostic disable-next-line: deprecated - vim.fn.termopen(vim.o.shell, { cwd = dir }) - end - elseif adapter.name == "ssh" then - local bufnr = vim.api.nvim_create_buf(false, true) - vim.api.nvim_set_current_buf(bufnr) - local url = require("oil.adapters.ssh").parse_url(bufname) - local cmd = require("oil.adapters.ssh.connection").create_ssh_command(url) - local term_id - if vim.fn.has("nvim-0.11") == 1 then - term_id = vim.fn.jobstart(cmd, { term = true }) - else - ---@diagnostic disable-next-line: deprecated - term_id = vim.fn.termopen(cmd) - end - if term_id then - vim.api.nvim_chan_send(term_id, string.format("cd %s\n", url.path)) - end - else - vim.notify( - string.format("Cannot open terminal for unsupported adapter: '%s'", adapter.name), - vim.log.levels.WARN - ) - end - end, -} - ----Copied from vim.ui.open in Neovim 0.10+ ----@param path string ----@return nil|string[] cmd ----@return nil|string error -local function get_open_cmd(path) - if vim.fn.has("mac") == 1 then - return { "open", path } - elseif vim.fn.has("win32") == 1 then - if vim.fn.executable("rundll32") == 1 then - return { "rundll32", "url.dll,FileProtocolHandler", path } - else - return nil, "rundll32 not found" - end - elseif vim.fn.executable("explorer.exe") == 1 then - return { "explorer.exe", path } - elseif vim.fn.executable("xdg-open") == 1 then - return { "xdg-open", path } - else - return nil, "no handler found" - end -end - -M.open_external = { - desc = "Open the entry under the cursor in an external program", - callback = function() - local entry = oil.get_cursor_entry() local dir = oil.get_current_dir() - if not entry or not dir then - return + if dir then + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_set_current_buf(bufnr) + vim.fn.termopen(vim.o.shell, { cwd = dir }) end - local path = dir .. entry.name - - if vim.ui.open then - vim.ui.open(path) - return - end - - local cmd, err = get_open_cmd(path) - if not cmd then - vim.notify(string.format("Could not open %s: %s", path, err), vim.log.levels.ERROR) - return - end - local jid = vim.fn.jobstart(cmd, { detach = true }) - assert(jid > 0, "Failed to start job") end, } M.refresh = { desc = "Refresh current directory list", - callback = function(opts) - opts = opts or {} - if vim.bo.modified and not opts.force then + callback = function() + if vim.bo.modified then local ok, choice = pcall(vim.fn.confirm, "Discard changes?", "No\nYes") if not ok or choice ~= 2 then return end end vim.cmd.edit({ bang = true }) - - -- :h CTRL-L-default - vim.cmd.nohlsearch() end, - parameters = { - force = { - desc = "When true, do not prompt user if they will be discarding changes", - type = "boolean", - }, - }, } -local function open_cmdline_with_path(path) - local escaped = - vim.api.nvim_replace_termcodes(": " .. vim.fn.fnameescape(path) .. "", true, false, true) - vim.api.nvim_feedkeys(escaped, "n", false) +local function open_cmdline_with_args(args) + local escaped = vim.api.nvim_replace_termcodes( + ": " .. args .. string.rep("", args:len() + 1), + true, + false, + true + ) + vim.api.nvim_feedkeys(escaped, "n", true) end M.open_cmdline = { desc = "Open vim cmdline with current entry as an argument", - callback = function(opts) - opts = vim.tbl_deep_extend("keep", opts or {}, { - shorten_path = true, - }) + callback = function() local config = require("oil.config") local fs = require("oil.fs") local entry = oil.get_cursor_entry() @@ -380,57 +188,13 @@ M.open_cmdline = { if not adapter or not path or adapter.name ~= "files" then return end - local fullpath = fs.posix_to_os_path(path) .. entry.name - if opts.modify then - fullpath = vim.fn.fnamemodify(fullpath, opts.modify) - end - if opts.shorten_path then - fullpath = fs.shorten_path(fullpath) - end - open_cmdline_with_path(fullpath) + local fullpath = fs.shorten_path(fs.posix_to_os_path(path) .. entry.name) + open_cmdline_with_args(fullpath) end, - parameters = { - modify = { - desc = "Modify the path with |fnamemodify()| using this as the mods argument", - type = "string", - }, - shorten_path = { - desc = "Use relative paths when possible", - type = "boolean", - }, - }, -} - -M.yank_entry = { - desc = "Yank the filepath of the entry under the cursor to a register", - callback = function(opts) - opts = opts or {} - local entry = oil.get_cursor_entry() - local dir = oil.get_current_dir() - if not entry or not dir then - return - end - local name = entry.name - if entry.type == "directory" then - name = name .. "/" - end - local path = dir .. name - if opts.modify then - path = vim.fn.fnamemodify(path, opts.modify) - end - vim.fn.setreg(vim.v.register, path) - end, - parameters = { - modify = { - desc = "Modify the path with |fnamemodify()| using this as the mods argument", - type = "string", - }, - }, } M.copy_entry_path = { desc = "Yank the filepath of the entry under the cursor to a register", - deprecated = true, callback = function() local entry = oil.get_cursor_entry() local dir = oil.get_current_dir() @@ -441,181 +205,17 @@ M.copy_entry_path = { end, } -M.copy_entry_filename = { - desc = "Yank the filename of the entry under the cursor to a register", - deprecated = true, - callback = function() - local entry = oil.get_cursor_entry() - if not entry then - return - end - vim.fn.setreg(vim.v.register, entry.name) - end, -} - -M.copy_to_system_clipboard = { - desc = "Copy the entry under the cursor to the system clipboard", - callback = function() - require("oil.clipboard").copy_to_system_clipboard() - end, -} - -M.paste_from_system_clipboard = { - desc = "Paste the system clipboard into the current oil directory", - callback = function(opts) - require("oil.clipboard").paste_from_system_clipboard(opts and opts.delete_original) - end, - parameters = { - delete_original = { - type = "boolean", - desc = "Delete the original file after copying", - }, - }, -} - M.open_cmdline_dir = { desc = "Open vim cmdline with current directory as an argument", - deprecated = true, callback = function() local fs = require("oil.fs") local dir = oil.get_current_dir() if dir then - open_cmdline_with_path(fs.shorten_path(dir)) + open_cmdline_with_args(fs.shorten_path(dir)) end end, } -M.change_sort = { - desc = "Change the sort order", - callback = function(opts) - opts = opts or {} - - if opts.sort then - oil.set_sort(opts.sort) - return - end - - local sort_cols = { "name", "size", "atime", "mtime", "ctime", "birthtime" } - vim.ui.select(sort_cols, { prompt = "Sort by", kind = "oil_sort_col" }, function(col) - if not col then - return - end - vim.ui.select( - { "ascending", "descending" }, - { prompt = "Sort order", kind = "oil_sort_order" }, - function(order) - if not order then - return - end - order = order == "ascending" and "asc" or "desc" - oil.set_sort({ - { "type", "asc" }, - { col, order }, - }) - end - ) - end) - end, - parameters = { - sort = { - type = "oil.SortSpec[]", - desc = "List of columns plus direction (see |oil.set_sort|) instead of interactive selection", - }, - }, -} - -M.toggle_trash = { - desc = "Jump to and from the trash for the current directory", - callback = function() - local fs = require("oil.fs") - local bufname = vim.api.nvim_buf_get_name(0) - local scheme, path = util.parse_url(bufname) - local bufnr = vim.api.nvim_get_current_buf() - local url - if scheme == "oil://" then - url = "oil-trash://" .. path - elseif scheme == "oil-trash://" then - url = "oil://" .. path - -- The non-linux trash implementations don't support per-directory trash, - -- so jump back to the stored source buffer. - if not fs.is_linux then - local src_bufnr = vim.b.oil_trash_toggle_src - if src_bufnr and vim.api.nvim_buf_is_valid(src_bufnr) then - url = vim.api.nvim_buf_get_name(src_bufnr) - end - end - else - vim.notify("No trash found for buffer", vim.log.levels.WARN) - return - end - vim.cmd.edit({ args = { url } }) - vim.b.oil_trash_toggle_src = bufnr - end, -} - -M.send_to_qflist = { - desc = "Sends files in the current oil directory to the quickfix list, replacing the previous entries.", - callback = function(opts) - opts = vim.tbl_deep_extend("keep", opts or {}, { - target = "qflist", - action = "r", - only_matching_search = false, - }) - util.send_to_quickfix({ - target = opts.target, - action = opts.action, - only_matching_search = opts.only_matching_search, - }) - end, - parameters = { - target = { - type = '"qflist"|"loclist"', - desc = "The target list to send files to", - }, - action = { - type = '"r"|"a"', - desc = "Replace or add to current quickfix list (see |setqflist-action|)", - }, - only_matching_search = { - type = "boolean", - desc = "Whether to only add the files that matches the last search. This option only applies when search highlighting is active", - }, - }, -} - -M.add_to_qflist = { - desc = "Adds files in the current oil directory to the quickfix list, keeping the previous entries.", - deprecated = true, - callback = function() - util.send_to_quickfix({ - target = "qflist", - mode = "a", - }) - end, -} - -M.send_to_loclist = { - desc = "Sends files in the current oil directory to the location list, replacing the previous entries.", - deprecated = true, - callback = function() - util.send_to_quickfix({ - target = "loclist", - mode = "r", - }) - end, -} - -M.add_to_loclist = { - desc = "Adds files in the current oil directory to the location list, keeping the previous entries.", - deprecated = true, - callback = function() - util.send_to_quickfix({ - target = "loclist", - mode = "a", - }) - end, -} - ---List actions for documentation generation ---@private M._get_actions = function() @@ -625,8 +225,6 @@ M._get_actions = function() table.insert(ret, { name = name, desc = action.desc, - deprecated = action.deprecated, - parameters = action.parameters, }) end end diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua index 9785c3a..ffc970d 100644 --- a/lua/oil/adapters/files.lua +++ b/lua/oil/adapters/files.lua @@ -3,31 +3,26 @@ local columns = require("oil.columns") local config = require("oil.config") local constants = require("oil.constants") local fs = require("oil.fs") -local git = require("oil.git") -local log = require("oil.log") local permissions = require("oil.adapters.files.permissions") +local trash = require("oil.adapters.files.trash") local util = require("oil.util") -local uv = vim.uv or vim.loop - local M = {} local FIELD_NAME = constants.FIELD_NAME -local FIELD_TYPE = constants.FIELD_TYPE local FIELD_META = constants.FIELD_META local function read_link_data(path, cb) - uv.fs_readlink( + vim.loop.fs_readlink( path, vim.schedule_wrap(function(link_err, link) if link_err then cb(link_err) else - assert(link) local stat_path = link if not fs.is_absolute(link) then stat_path = fs.join(vim.fn.fnamemodify(path, ":h"), link) end - uv.fs_stat(stat_path, function(stat_err, stat) + vim.loop.fs_stat(stat_path, function(stat_err, stat) cb(nil, link, stat) end) end @@ -35,30 +30,35 @@ local function read_link_data(path, cb) ) end ----@class (exact) oil.FilesAdapter: oil.Adapter ----@field to_short_os_path fun(path: string, entry_type: nil|oil.EntryType): string - ---@param path string ---@param entry_type nil|oil.EntryType ---@return string M.to_short_os_path = function(path, entry_type) local shortpath = fs.shorten_path(fs.posix_to_os_path(path)) if entry_type == "directory" then - shortpath = util.addslash(shortpath, true) + shortpath = util.addslash(shortpath) end return shortpath end local file_columns = {} +local fs_stat_meta_fields = { + stat = function(parent_url, entry, cb) + local _, path = util.parse_url(parent_url) + local dir = fs.posix_to_os_path(path) + vim.loop.fs_stat(fs.join(dir, entry[FIELD_NAME]), cb) + end, +} + file_columns.size = { - require_stat = true, + meta_fields = fs_stat_meta_fields, render = function(entry, conf) local meta = entry[FIELD_META] - local stat = meta and meta.stat + local stat = meta.stat if not stat then - return columns.EMPTY + return "" end if stat.size >= 1e9 then return string.format("%.1fG", stat.size / 1e9) @@ -71,16 +71,6 @@ file_columns.size = { end end, - get_sort_value = function(entry) - local meta = entry[FIELD_META] - local stat = meta and meta.stat - if stat then - return stat.size - else - return 0 - end - end, - parse = function(line, conf) return line:match("^(%d+%S*)%s+(.*)$") end, @@ -89,13 +79,13 @@ file_columns.size = { -- TODO support file permissions on windows if not fs.is_windows then file_columns.permissions = { - require_stat = true, + meta_fields = fs_stat_meta_fields, render = function(entry, conf) local meta = entry[FIELD_META] - local stat = meta and meta.stat + local stat = meta.stat if not stat then - return columns.EMPTY + return "" end return permissions.mode_to_str(stat.mode) end, @@ -106,7 +96,7 @@ if not fs.is_windows then compare = function(entry, parsed_value) local meta = entry[FIELD_META] - if parsed_value and meta and meta.stat and meta.stat.mode then + if parsed_value and meta.stat and meta.stat.mode then local mask = bit.lshift(1, 12) - 1 local old_mode = bit.band(meta.stat.mode, mask) if parsed_value ~= old_mode then @@ -118,7 +108,6 @@ if not fs.is_windows then render_action = function(action) local _, path = util.parse_url(action.url) - assert(path) return string.format( "CHMOD %s %s", permissions.mode_to_octal_str(action.value), @@ -128,38 +117,29 @@ if not fs.is_windows then perform_action = function(action, callback) local _, path = util.parse_url(action.url) - assert(path) path = fs.posix_to_os_path(path) - uv.fs_stat(path, function(err, stat) + vim.loop.fs_stat(path, function(err, stat) if err then return callback(err) end - assert(stat) -- We are only changing the lower 12 bits of the mode local mask = bit.bnot(bit.lshift(1, 12) - 1) local old_mode = bit.band(stat.mode, mask) - uv.fs_chmod(path, bit.bor(old_mode, action.value), callback) + vim.loop.fs_chmod(path, bit.bor(old_mode, action.value), callback) end) end, } end -local current_year --- Make sure we run this import-time effect in the main loop (mostly for tests) -vim.schedule(function() - current_year = vim.fn.strftime("%Y") -end) +local current_year = vim.fn.strftime("%Y") for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do file_columns[time_key] = { - require_stat = true, + meta_fields = fs_stat_meta_fields, render = function(entry, conf) local meta = entry[FIELD_META] - local stat = meta and meta.stat - if not stat then - return columns.EMPTY - end + local stat = meta.stat local fmt = conf and conf.format local ret if fmt then @@ -179,52 +159,15 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do local fmt = conf and conf.format local pattern if fmt then - -- Replace placeholders with a pattern that matches non-space characters (e.g. %H -> %S+) - -- and whitespace with a pattern that matches any amount of whitespace - -- e.g. "%b %d %Y" -> "%S+%s+%S+%s+%S+" - pattern = fmt - :gsub("%%.", "%%S+") - :gsub("%s+", "%%s+") - -- escape `()[]` because those are special characters in Lua patterns - :gsub( - "%(", - "%%(" - ) - :gsub("%)", "%%)") - :gsub("%[", "%%[") - :gsub("%]", "%%]") + pattern = fmt:gsub("%%.", "%%S+") else pattern = "%S+%s+%d+%s+%d%d:?%d%d" end return line:match("^(" .. pattern .. ")%s+(.+)$") end, - - get_sort_value = function(entry) - local meta = entry[FIELD_META] - local stat = meta and meta.stat - if stat then - return stat[time_key].sec - else - return 0 - end - end, } end ----@param column_defs table[] ----@return boolean -local function columns_require_stat(column_defs) - for _, def in ipairs(column_defs) do - local name = util.split_config(def) - local column = M.get_column(name) - ---@diagnostic disable-next-line: undefined-field We only put this on the files adapter columns - if column and column.require_stat then - return true - end - end - return false -end - ---@param name string ---@return nil|oil.ColumnDefinition M.get_column = function(name) @@ -235,37 +178,16 @@ end ---@param callback fun(url: string) M.normalize_url = function(url, callback) local scheme, path = util.parse_url(url) - assert(path) - - if fs.is_windows then - if path == "/" then - return callback(url) - else - local is_root_drive = path:match("^/%u$") - if is_root_drive then - return callback(url .. "/") - end - end - end - local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p") - uv.fs_realpath(os_path, function(err, new_os_path) - local realpath - if fs.is_windows then - -- Ignore the fs_realpath on windows because it will resolve mapped network drives to the IP - -- address instead of using the drive letter - realpath = os_path - else - realpath = new_os_path or os_path - end - - uv.fs_stat( + vim.loop.fs_realpath(os_path, function(err, new_os_path) + local realpath = new_os_path or os_path + vim.loop.fs_stat( realpath, vim.schedule_wrap(function(stat_err, stat) local is_directory if stat then is_directory = stat.type == "directory" - elseif vim.endswith(realpath, "/") or (fs.is_windows and vim.endswith(realpath, "\\")) then + elseif vim.endswith(realpath, "/") then is_directory = true else local filetype = vim.filetype.match({ filename = vim.fs.basename(realpath) }) @@ -276,182 +198,28 @@ M.normalize_url = function(url, callback) local norm_path = util.addslash(fs.os_to_posix_path(realpath)) callback(scheme .. norm_path) else - callback(vim.fn.fnamemodify(realpath, ":.")) + callback(realpath) end end) ) end) end ----@param url string ----@param entry oil.Entry ----@param cb fun(path: nil|string) -M.get_entry_path = function(url, entry, cb) - if entry.id then - local parent_url = cache.get_parent_url(entry.id) - local scheme, path = util.parse_url(parent_url) - M.normalize_url(scheme .. path .. entry.name, cb) - else - if entry.type == "directory" then - cb(url) - else - local _, path = util.parse_url(url) - local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(assert(path)), ":p") - cb(os_path) - end - end -end - ----@param parent_dir string ----@param entry oil.InternalEntry ----@param require_stat boolean ----@param cb fun(err?: string) -local function fetch_entry_metadata(parent_dir, entry, require_stat, cb) - local entry_path = fs.posix_to_os_path(parent_dir .. entry[FIELD_NAME]) - local meta = entry[FIELD_META] - if not meta then - meta = {} - entry[FIELD_META] = meta - end - - -- Sometimes fs_readdir entries don't have a type, so we need to stat them. - -- See https://github.com/stevearc/oil.nvim/issues/543 - if not require_stat and not entry[FIELD_TYPE] then - require_stat = true - end - - -- Make sure we always get fs_stat info for links - if entry[FIELD_TYPE] == "link" then - read_link_data(entry_path, function(link_err, link, link_stat) - if link_err then - log.warn("Error reading link data %s: %s", entry_path, link_err) - return cb() - end - meta.link = link - if link_stat then - -- Use the fstat of the linked file as the stat for the link - meta.link_stat = link_stat - meta.stat = link_stat - elseif require_stat then - -- The link is broken, so let's use the stat of the link itself - uv.fs_lstat(entry_path, function(stat_err, stat) - if stat_err then - log.warn("Error lstat link file %s: %s", entry_path, stat_err) - return cb() - end - meta.stat = stat - cb() - end) - return - end - - cb() - end) - elseif require_stat then - uv.fs_stat(entry_path, function(stat_err, stat) - if stat_err then - log.warn("Error stat file %s: %s", entry_path, stat_err) - return cb() - end - assert(stat) - entry[FIELD_TYPE] = stat.type - meta.stat = stat - cb() - end) - else - cb() - end -end - --- On windows, sometimes the entry type from fs_readdir is "link" but the actual type is not. --- See https://github.com/stevearc/oil.nvim/issues/535 -if fs.is_windows then - local old_fetch_metadata = fetch_entry_metadata - fetch_entry_metadata = function(parent_dir, entry, require_stat, cb) - if entry[FIELD_TYPE] == "link" then - local entry_path = fs.posix_to_os_path(parent_dir .. entry[FIELD_NAME]) - uv.fs_lstat(entry_path, function(stat_err, stat) - if stat_err then - log.warn("Error lstat link file %s: %s", entry_path, stat_err) - return old_fetch_metadata(parent_dir, entry, require_stat, cb) - end - assert(stat) - entry[FIELD_TYPE] = stat.type - local meta = entry[FIELD_META] - if not meta then - meta = {} - entry[FIELD_META] = meta - end - meta.stat = stat - old_fetch_metadata(parent_dir, entry, require_stat, cb) - end) - else - return old_fetch_metadata(parent_dir, entry, require_stat, cb) - end - end -end - ---@param url string ---@param column_defs string[] ----@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) -local function list_windows_drives(url, column_defs, cb) +---@param callback fun(err: nil|string, entries: nil|oil.InternalEntry[]) +M.list = function(url, column_defs, callback) local _, path = util.parse_url(url) - assert(path) - local require_stat = columns_require_stat(column_defs) - local stdout = "" - local jid = vim.fn.jobstart({ "wmic", "logicaldisk", "get", "name" }, { - stdout_buffered = true, - on_stdout = function(_, data) - stdout = table.concat(data, "\n") - end, - on_exit = function(_, code) - if code ~= 0 then - return cb("Error listing windows devices") - end - local lines = vim.split(stdout, "\n", { plain = true, trimempty = true }) - -- Remove the "Name" header - table.remove(lines, 1) - local internal_entries = {} - local complete_disk_cb = util.cb_collect(#lines, function(err) - if err then - cb(err) - else - cb(nil, internal_entries) - end - end) - - for _, disk in ipairs(lines) do - if disk:match("^%s*$") then - -- Skip empty line - complete_disk_cb() - else - disk = disk:gsub(":%s*$", "") - local cache_entry = cache.create_entry(url, disk, "directory") - table.insert(internal_entries, cache_entry) - fetch_entry_metadata(path, cache_entry, require_stat, complete_disk_cb) - end - end - end, - }) - if jid <= 0 then - cb("Could not list windows devices") - end -end - ----@param url string ----@param column_defs string[] ----@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) -M.list = function(url, column_defs, cb) - local _, path = util.parse_url(url) - assert(path) - if fs.is_windows and path == "/" then - return list_windows_drives(url, column_defs, cb) - end local dir = fs.posix_to_os_path(path) - local require_stat = columns_require_stat(column_defs) - - ---@diagnostic disable-next-line: param-type-mismatch, discard-returns - uv.fs_opendir(dir, function(open_err, fd) + local fetch_meta = columns.get_metadata_fetcher(M, column_defs) + cache.begin_update_url(url) + local function cb(err, data) + if err or not data then + cache.end_update_url(url) + end + callback(err, data) + end + vim.loop.fs_opendir(dir, function(open_err, fd) if open_err then if open_err:match("^ENOENT: no such file or directory") then -- If the directory doesn't exist, treat the list as a success. We will be able to traverse @@ -462,11 +230,14 @@ M.list = function(url, column_defs, cb) end end local read_next - read_next = function() - uv.fs_readdir(fd, function(err, entries) - local internal_entries = {} + read_next = function(read_err) + if read_err then + cb(read_err) + return + end + vim.loop.fs_readdir(fd, function(err, entries) if err then - uv.fs_closedir(fd, function() + vim.loop.fs_closedir(fd, function() cb(err) end) return @@ -475,16 +246,42 @@ M.list = function(url, column_defs, cb) if inner_err then cb(inner_err) else - cb(nil, internal_entries, read_next) + cb(nil, true) + read_next() end end) for _, entry in ipairs(entries) do local cache_entry = cache.create_entry(url, entry.name, entry.type) - table.insert(internal_entries, cache_entry) - fetch_entry_metadata(path, cache_entry, require_stat, poll) + fetch_meta(url, cache_entry, function(meta_err) + if err then + poll(meta_err) + else + local meta = cache_entry[FIELD_META] + -- Make sure we always get fs_stat info for links + if entry.type == "link" then + read_link_data(fs.join(dir, entry.name), function(link_err, link, link_stat) + if link_err then + poll(link_err) + else + if not meta then + meta = {} + cache_entry[FIELD_META] = meta + end + meta.link = link + meta.link_stat = link_stat + cache.store_entry(url, cache_entry) + poll() + end + end) + else + cache.store_entry(url, cache_entry) + poll() + end + end + end) end else - uv.fs_closedir(fd, function(close_err) + vim.loop.fs_closedir(fd, function(close_err) if close_err then cb(close_err) else @@ -495,8 +292,7 @@ M.list = function(url, column_defs, cb) end) end read_next() - ---@diagnostic disable-next-line: param-type-mismatch - end, 10000) + end, 100) -- TODO do some testing for this end ---@param bufnr integer @@ -504,18 +300,28 @@ end M.is_modifiable = function(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr) local _, path = util.parse_url(bufname) - assert(path) - if fs.is_windows and path == "/" then - return false - end local dir = fs.posix_to_os_path(path) - local stat = uv.fs_stat(dir) + local stat = vim.loop.fs_stat(dir) if not stat then return true end - -- fs_access can return nil, force boolean return - return uv.fs_access(dir, "W") == true + -- Can't do permissions checks on windows + if fs.is_windows then + return true + end + + local uid = vim.loop.getuid() + local gid = vim.loop.getgid() + local rwx + if uid == stat.uid then + rwx = bit.rshift(stat.mode, 6) + elseif gid == stat.gid then + rwx = bit.rshift(stat.mode, 3) + else + rwx = stat.mode + end + return bit.band(rwx, 2) ~= 0 end ---@param action oil.Action @@ -523,7 +329,6 @@ end M.render_action = function(action) if action.type == "create" then local _, path = util.parse_url(action.url) - assert(path) local ret = string.format("CREATE %s", M.to_short_os_path(path, action.entry_type)) if action.link then ret = ret .. " -> " .. fs.posix_to_os_path(action.link) @@ -531,20 +336,12 @@ M.render_action = function(action) return ret elseif action.type == "delete" then local _, path = util.parse_url(action.url) - assert(path) - local short_path = M.to_short_os_path(path, action.entry_type) - if config.delete_to_trash then - return string.format(" TRASH %s", short_path) - else - return string.format("DELETE %s", short_path) - end + return string.format("DELETE %s", M.to_short_os_path(path, action.entry_type)) elseif action.type == "move" or action.type == "copy" then - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) + local dest_adapter = config.get_adapter_by_scheme(action.dest_url) if dest_adapter == M then local _, src_path = util.parse_url(action.src_url) - assert(src_path) local _, dest_path = util.parse_url(action.dest_url) - assert(dest_path) return string.format( " %s %s -> %s", action.type:upper(), @@ -552,7 +349,7 @@ M.render_action = function(action) M.to_short_os_path(dest_path, action.entry_type) ) else - -- We should never hit this because we don't implement supported_cross_adapter_actions + -- We should never hit this because we don't implement supports_xfer error("files adapter doesn't support cross-adapter move/copy") end else @@ -565,22 +362,9 @@ end M.perform_action = function(action, cb) if action.type == "create" then local _, path = util.parse_url(action.url) - assert(path) path = fs.posix_to_os_path(path) - - if config.git.add(path) then - local old_cb = cb - cb = vim.schedule_wrap(function(err) - if not err then - git.add(path, old_cb) - else - old_cb(err) - end - end) - end - if action.entry_type == "directory" then - uv.fs_mkdir(path, 493, function(err) + vim.loop.fs_mkdir(path, 493, function(err) -- Ignore if the directory already exists if not err or err:match("^EEXIST:") then cb() @@ -597,62 +381,40 @@ M.perform_action = function(action, cb) junction = false, } end - ---@diagnostic disable-next-line: param-type-mismatch - uv.fs_symlink(target, path, flags, cb) + vim.loop.fs_symlink(target, path, flags, cb) else fs.touch(path, cb) end elseif action.type == "delete" then local _, path = util.parse_url(action.url) - assert(path) path = fs.posix_to_os_path(path) - - if config.git.rm(path) then - local old_cb = cb - cb = vim.schedule_wrap(function(err) - if not err then - git.rm(path, old_cb) - else - old_cb(err) - end - end) - end - if config.delete_to_trash then - require("oil.adapters.trash").delete_to_trash(path, cb) + trash.recursive_delete(path, cb) else fs.recursive_delete(action.entry_type, path, cb) end elseif action.type == "move" then - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) + local dest_adapter = config.get_adapter_by_scheme(action.dest_url) if dest_adapter == M then local _, src_path = util.parse_url(action.src_url) - assert(src_path) local _, dest_path = util.parse_url(action.dest_url) - assert(dest_path) src_path = fs.posix_to_os_path(src_path) dest_path = fs.posix_to_os_path(dest_path) - if config.git.mv(src_path, dest_path) then - git.mv(action.entry_type, src_path, dest_path, cb) - else - fs.recursive_move(action.entry_type, src_path, dest_path, cb) - end + fs.recursive_move(action.entry_type, src_path, dest_path, vim.schedule_wrap(cb)) else - -- We should never hit this because we don't implement supported_cross_adapter_actions + -- We should never hit this because we don't implement supports_xfer cb("files adapter doesn't support cross-adapter move") end elseif action.type == "copy" then - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) + local dest_adapter = config.get_adapter_by_scheme(action.dest_url) if dest_adapter == M then local _, src_path = util.parse_url(action.src_url) - assert(src_path) local _, dest_path = util.parse_url(action.dest_url) - assert(dest_path) src_path = fs.posix_to_os_path(src_path) dest_path = fs.posix_to_os_path(dest_path) fs.recursive_copy(action.entry_type, src_path, dest_path, cb) else - -- We should never hit this because we don't implement supported_cross_adapter_actions + -- We should never hit this because we don't implement supports_xfer cb("files adapter doesn't support cross-adapter copy") end else diff --git a/lua/oil/adapters/files/permissions.lua b/lua/oil/adapters/files/permissions.lua index 6c306a6..cf50b55 100644 --- a/lua/oil/adapters/files/permissions.lua +++ b/lua/oil/adapters/files/permissions.lua @@ -1,6 +1,6 @@ local M = {} ----@param exe_modifier false|string +---@param exe_modifier nil|false|string ---@param num integer ---@return string local function perm_to_str(exe_modifier, num) diff --git a/lua/oil/adapters/files/trash.lua b/lua/oil/adapters/files/trash.lua new file mode 100644 index 0000000..37769f7 --- /dev/null +++ b/lua/oil/adapters/files/trash.lua @@ -0,0 +1,39 @@ +local config = require("oil.config") +local M = {} + +M.recursive_delete = function(path, cb) + local stdout = {} + local stderr = {} + local cmd = vim.list_extend(vim.split(config.trash_command, "%s+"), { path }) + local jid = vim.fn.jobstart(cmd, { + stdout_buffered = true, + stderr_buffered = true, + on_stdout = function(j, output) + stdout = output + end, + on_stderr = function(j, output) + stderr = output + end, + on_exit = function(j, exit_code) + if exit_code == 0 then + cb() + else + cb( + string.format( + "Error moving '%s' to trash:\n stdout: %s\n stderr: %s", + path, + table.concat(stdout, "\n "), + table.concat(stderr, "\n ") + ) + ) + end + end, + }) + if jid == 0 then + cb(string.format("Passed invalid argument '%s' to '%s'", path, config.trash_command)) + elseif jid == -1 then + cb(string.format("'%s' is not executable", config.trash_command)) + end +end + +return M diff --git a/lua/oil/adapters/s3.lua b/lua/oil/adapters/s3.lua deleted file mode 100644 index b7b5400..0000000 --- a/lua/oil/adapters/s3.lua +++ /dev/null @@ -1,389 +0,0 @@ -local config = require("oil.config") -local constants = require("oil.constants") -local files = require("oil.adapters.files") -local fs = require("oil.fs") -local loading = require("oil.loading") -local pathutil = require("oil.pathutil") -local s3fs = require("oil.adapters.s3.s3fs") -local util = require("oil.util") -local M = {} - -local FIELD_META = constants.FIELD_META - ----@class (exact) oil.s3Url ----@field scheme string ----@field bucket nil|string ----@field path nil|string - ----@param oil_url string ----@return oil.s3Url -M.parse_url = function(oil_url) - local scheme, url = util.parse_url(oil_url) - assert(scheme and url, string.format("Malformed input url '%s'", oil_url)) - local ret = { scheme = scheme } - local bucket, path = url:match("^([^/]+)/?(.*)$") - ret.bucket = bucket - ret.path = path ~= "" and path or nil - if not ret.bucket and ret.path then - error(string.format("Parsing error for s3 url: %s", oil_url)) - end - ---@cast ret oil.s3Url - return ret -end - ----@param url oil.s3Url ----@return string -local function url_to_str(url) - local pieces = { url.scheme } - if url.bucket then - assert(url.bucket ~= "") - table.insert(pieces, url.bucket) - table.insert(pieces, "/") - end - if url.path then - assert(url.path ~= "") - table.insert(pieces, url.path) - end - return table.concat(pieces, "") -end - ----@param url oil.s3Url ----@param is_folder boolean ----@return string -local function url_to_s3(url, is_folder) - local pieces = { "s3://" } - if url.bucket then - assert(url.bucket ~= "") - table.insert(pieces, url.bucket) - table.insert(pieces, "/") - end - if url.path then - assert(url.path ~= "") - table.insert(pieces, url.path) - if is_folder and not vim.endswith(url.path, "/") then - table.insert(pieces, "/") - end - end - return table.concat(pieces, "") -end - ----@param url oil.s3Url ----@return boolean -local function is_bucket(url) - assert(url.bucket and url.bucket ~= "") - if url.path then - assert(url.path ~= "") - return false - end - return true -end - -local s3_columns = {} -s3_columns.size = { - render = function(entry, conf) - local meta = entry[FIELD_META] - if not meta or not meta.size then - return "" - elseif meta.size >= 1e9 then - return string.format("%.1fG", meta.size / 1e9) - elseif meta.size >= 1e6 then - return string.format("%.1fM", meta.size / 1e6) - elseif meta.size >= 1e3 then - return string.format("%.1fk", meta.size / 1e3) - else - return string.format("%d", meta.size) - end - end, - - parse = function(line, conf) - return line:match("^(%d+%S*)%s+(.*)$") - end, - - get_sort_value = function(entry) - local meta = entry[FIELD_META] - if meta and meta.size then - return meta.size - else - return 0 - end - end, -} - -s3_columns.birthtime = { - render = function(entry, conf) - local meta = entry[FIELD_META] - if not meta or not meta.date then - return "" - else - return meta.date - end - end, - - parse = function(line, conf) - return line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$") - end, - - get_sort_value = function(entry) - local meta = entry[FIELD_META] - if meta and meta.date then - local year, month, day, hour, min, sec = - meta.date:match("^(%d+)%-(%d+)%-(%d+)%s(%d+):(%d+):(%d+)$") - local time = - os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec }) - return time - else - return 0 - end - end, -} - ----@param name string ----@return nil|oil.ColumnDefinition -M.get_column = function(name) - return s3_columns[name] -end - ----@param bufname string ----@return string -M.get_parent = function(bufname) - local res = M.parse_url(bufname) - if res.path then - assert(res.path ~= "") - local path = pathutil.parent(res.path) - res.path = path ~= "" and path or nil - else - res.bucket = nil - end - return url_to_str(res) -end - ----@param url string ----@param callback fun(url: string) -M.normalize_url = function(url, callback) - local res = M.parse_url(url) - callback(url_to_str(res)) -end - ----@param url string ----@param column_defs string[] ----@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) -M.list = function(url, column_defs, callback) - if vim.fn.executable("aws") ~= 1 then - callback("`aws` is not executable. Can you run `aws s3 ls`?") - return - end - - local res = M.parse_url(url) - s3fs.list_dir(url, url_to_s3(res, true), callback) -end - ----@param bufnr integer ----@return boolean -M.is_modifiable = function(bufnr) - -- default assumption is that everything is modifiable - return true -end - ----@param action oil.Action ----@return string -M.render_action = function(action) - local is_folder = action.entry_type == "directory" - if action.type == "create" then - local res = M.parse_url(action.url) - local extra = is_bucket(res) and "BUCKET " or "" - return string.format("CREATE %s%s", extra, url_to_s3(res, is_folder)) - elseif action.type == "delete" then - local res = M.parse_url(action.url) - local extra = is_bucket(res) and "BUCKET " or "" - return string.format("DELETE %s%s", extra, url_to_s3(res, is_folder)) - elseif action.type == "move" or action.type == "copy" then - local src = action.src_url - local dest = action.dest_url - if config.get_adapter_by_scheme(src) ~= M then - local _, path = util.parse_url(src) - assert(path) - src = files.to_short_os_path(path, action.entry_type) - dest = url_to_s3(M.parse_url(dest), is_folder) - elseif config.get_adapter_by_scheme(dest) ~= M then - local _, path = util.parse_url(dest) - assert(path) - dest = files.to_short_os_path(path, action.entry_type) - src = url_to_s3(M.parse_url(src), is_folder) - end - return string.format(" %s %s -> %s", action.type:upper(), src, dest) - else - error(string.format("Bad action type: '%s'", action.type)) - end -end - ----@param action oil.Action ----@param cb fun(err: nil|string) -M.perform_action = function(action, cb) - local is_folder = action.entry_type == "directory" - if action.type == "create" then - local res = M.parse_url(action.url) - local bucket = is_bucket(res) - - if action.entry_type == "directory" and bucket then - s3fs.mb(url_to_s3(res, true), cb) - elseif action.entry_type == "directory" or action.entry_type == "file" then - s3fs.touch(url_to_s3(res, is_folder), cb) - else - cb(string.format("Bad entry type on s3 create action: %s", action.entry_type)) - end - elseif action.type == "delete" then - local res = M.parse_url(action.url) - local bucket = is_bucket(res) - - if action.entry_type == "directory" and bucket then - s3fs.rb(url_to_s3(res, true), cb) - elseif action.entry_type == "directory" or action.entry_type == "file" then - s3fs.rm(url_to_s3(res, is_folder), is_folder, cb) - else - cb(string.format("Bad entry type on s3 delete action: %s", action.entry_type)) - end - elseif action.type == "move" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - if - (src_adapter ~= M and src_adapter ~= files) or (dest_adapter ~= M and dest_adapter ~= files) - then - cb( - string.format( - "We should never attempt to move from the %s adapter to the %s adapter.", - src_adapter.name, - dest_adapter.name - ) - ) - end - - local src, _ - if src_adapter == M then - local src_res = M.parse_url(action.src_url) - src = url_to_s3(src_res, is_folder) - else - _, src = util.parse_url(action.src_url) - end - assert(src) - - local dest - if dest_adapter == M then - local dest_res = M.parse_url(action.dest_url) - dest = url_to_s3(dest_res, is_folder) - else - _, dest = util.parse_url(action.dest_url) - end - assert(dest) - - s3fs.mv(src, dest, is_folder, cb) - elseif action.type == "copy" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - if - (src_adapter ~= M and src_adapter ~= files) or (dest_adapter ~= M and dest_adapter ~= files) - then - cb( - string.format( - "We should never attempt to copy from the %s adapter to the %s adapter.", - src_adapter.name, - dest_adapter.name - ) - ) - end - - local src, _ - if src_adapter == M then - local src_res = M.parse_url(action.src_url) - src = url_to_s3(src_res, is_folder) - else - _, src = util.parse_url(action.src_url) - end - assert(src) - - local dest - if dest_adapter == M then - local dest_res = M.parse_url(action.dest_url) - dest = url_to_s3(dest_res, is_folder) - else - _, dest = util.parse_url(action.dest_url) - end - assert(dest) - - s3fs.cp(src, dest, is_folder, cb) - else - cb(string.format("Bad action type: %s", action.type)) - end -end - -M.supported_cross_adapter_actions = { files = "move" } - ----@param bufnr integer -M.read_file = function(bufnr) - loading.set_loading(bufnr, true) - local bufname = vim.api.nvim_buf_get_name(bufnr) - local url = M.parse_url(bufname) - local basename = pathutil.basename(bufname) - local cache_dir = vim.fn.stdpath("cache") - assert(type(cache_dir) == "string") - local tmpdir = fs.join(cache_dir, "oil") - fs.mkdirp(tmpdir) - local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "s3_XXXXXX")) - if fd then - vim.loop.fs_close(fd) - end - local tmp_bufnr = vim.fn.bufadd(tmpfile) - - s3fs.cp(url_to_s3(url, false), tmpfile, false, function(err) - loading.set_loading(bufnr, false) - vim.bo[bufnr].modifiable = true - vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { silent = true } }) - if err then - vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, vim.split(err, "\n")) - else - vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {}) - vim.api.nvim_buf_call(bufnr, function() - vim.cmd.read({ args = { tmpfile }, mods = { silent = true } }) - end) - vim.loop.fs_unlink(tmpfile) - vim.api.nvim_buf_set_lines(bufnr, 0, 1, true, {}) - end - vim.bo[bufnr].modified = false - local filetype = vim.filetype.match({ buf = bufnr, filename = basename }) - if filetype then - vim.bo[bufnr].filetype = filetype - end - vim.cmd.doautocmd({ args = { "BufReadPost", bufname }, mods = { silent = true } }) - vim.api.nvim_buf_delete(tmp_bufnr, { force = true }) - end) -end - ----@param bufnr integer -M.write_file = function(bufnr) - local bufname = vim.api.nvim_buf_get_name(bufnr) - local url = M.parse_url(bufname) - local cache_dir = vim.fn.stdpath("cache") - assert(type(cache_dir) == "string") - local tmpdir = fs.join(cache_dir, "oil") - local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "s3_XXXXXXXX")) - if fd then - vim.loop.fs_close(fd) - end - vim.cmd.doautocmd({ args = { "BufWritePre", bufname }, mods = { silent = true } }) - vim.bo[bufnr].modifiable = false - vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } }) - local tmp_bufnr = vim.fn.bufadd(tmpfile) - - s3fs.cp(tmpfile, url_to_s3(url, false), false, function(err) - vim.bo[bufnr].modifiable = true - if err then - vim.notify(string.format("Error writing file: %s", err), vim.log.levels.ERROR) - else - vim.bo[bufnr].modified = false - vim.cmd.doautocmd({ args = { "BufWritePost", bufname }, mods = { silent = true } }) - end - vim.loop.fs_unlink(tmpfile) - vim.api.nvim_buf_delete(tmp_bufnr, { force = true }) - end) -end - -return M diff --git a/lua/oil/adapters/s3/s3fs.lua b/lua/oil/adapters/s3/s3fs.lua deleted file mode 100644 index 2e16307..0000000 --- a/lua/oil/adapters/s3/s3fs.lua +++ /dev/null @@ -1,149 +0,0 @@ -local cache = require("oil.cache") -local config = require("oil.config") -local constants = require("oil.constants") -local shell = require("oil.shell") -local util = require("oil.util") - -local M = {} - -local FIELD_META = constants.FIELD_META - ----@param line string ----@return string Name of entry ----@return oil.EntryType ----@return table Metadata for entry -local function parse_ls_line_bucket(line) - local date, name = line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$") - if not date or not name then - error(string.format("Could not parse '%s'", line)) - end - local type = "directory" - local meta = { date = date } - return name, type, meta -end - ----@param line string ----@return string Name of entry ----@return oil.EntryType ----@return table Metadata for entry -local function parse_ls_line_file(line) - local name = line:match("^%s+PRE%s+(.*)/$") - local type = "directory" - local meta = {} - if name then - return name, type, meta - end - local date, size - date, size, name = line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(%d+)%s+(.*)$") - if not name then - error(string.format("Could not parse '%s'", line)) - end - type = "file" - meta = { date = date, size = tonumber(size) } - return name, type, meta -end - ----@param cmd string[] cmd and flags ----@return string[] Shell command to run -local function create_s3_command(cmd) - local full_cmd = vim.list_extend({ "aws", "s3" }, cmd) - return vim.list_extend(full_cmd, config.extra_s3_args) -end - ----@param url string ----@param path string ----@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) -function M.list_dir(url, path, callback) - local cmd = create_s3_command({ "ls", path, "--color=off", "--no-cli-pager" }) - shell.run(cmd, function(err, lines) - if err then - return callback(err) - end - assert(lines) - local cache_entries = {} - local url_path, _ - _, url_path = util.parse_url(url) - local is_top_level = url_path == nil or url_path:match("/") == nil - local parse_ls_line = is_top_level and parse_ls_line_bucket or parse_ls_line_file - for _, line in ipairs(lines) do - if line ~= "" then - local name, type, meta = parse_ls_line(line) - -- in s3 '-' can be used to create an "empty folder" - if name ~= "-" then - local cache_entry = cache.create_entry(url, name, type) - table.insert(cache_entries, cache_entry) - cache_entry[FIELD_META] = meta - end - end - end - callback(nil, cache_entries) - end) -end - ---- Create files ----@param path string ----@param callback fun(err: nil|string) -function M.touch(path, callback) - -- here "-" means that we copy from stdin - local cmd = create_s3_command({ "cp", "-", path }) - shell.run(cmd, { stdin = "null" }, callback) -end - ---- Remove files ----@param path string ----@param is_folder boolean ----@param callback fun(err: nil|string) -function M.rm(path, is_folder, callback) - local main_cmd = { "rm", path } - if is_folder then - table.insert(main_cmd, "--recursive") - end - local cmd = create_s3_command(main_cmd) - shell.run(cmd, callback) -end - ---- Remove bucket ----@param bucket string ----@param callback fun(err: nil|string) -function M.rb(bucket, callback) - local cmd = create_s3_command({ "rb", bucket }) - shell.run(cmd, callback) -end - ---- Make bucket ----@param bucket string ----@param callback fun(err: nil|string) -function M.mb(bucket, callback) - local cmd = create_s3_command({ "mb", bucket }) - shell.run(cmd, callback) -end - ---- Move files ----@param src string ----@param dest string ----@param is_folder boolean ----@param callback fun(err: nil|string) -function M.mv(src, dest, is_folder, callback) - local main_cmd = { "mv", src, dest } - if is_folder then - table.insert(main_cmd, "--recursive") - end - local cmd = create_s3_command(main_cmd) - shell.run(cmd, callback) -end - ---- Copy files ----@param src string ----@param dest string ----@param is_folder boolean ----@param callback fun(err: nil|string) -function M.cp(src, dest, is_folder, callback) - local main_cmd = { "cp", src, dest } - if is_folder then - table.insert(main_cmd, "--recursive") - end - local cmd = create_s3_command(main_cmd) - shell.run(cmd, callback) -end - -return M diff --git a/lua/oil/adapters/ssh.lua b/lua/oil/adapters/ssh.lua index ae4291f..d8cc33b 100644 --- a/lua/oil/adapters/ssh.lua +++ b/lua/oil/adapters/ssh.lua @@ -1,37 +1,29 @@ +local cache = require("oil.cache") local config = require("oil.config") local constants = require("oil.constants") -local files = require("oil.adapters.files") local fs = require("oil.fs") +local files = require("oil.adapters.files") local loading = require("oil.loading") -local pathutil = require("oil.pathutil") local permissions = require("oil.adapters.files.permissions") -local shell = require("oil.shell") local sshfs = require("oil.adapters.ssh.sshfs") +local pathutil = require("oil.pathutil") +local shell = require("oil.shell") local util = require("oil.util") local M = {} -local FIELD_NAME = constants.FIELD_NAME local FIELD_META = constants.FIELD_META ----@class (exact) oil.sshUrl +---@class oil.sshUrl ---@field scheme string ---@field host string ---@field user nil|string ---@field port nil|integer ---@field path string ----@param args string[] -local function scp(args, ...) - local cmd = vim.list_extend({ "scp", "-C" }, config.extra_scp_args) - vim.list_extend(cmd, args) - shell.run(cmd, ...) -end - ---@param oil_url string ---@return oil.sshUrl -M.parse_url = function(oil_url) +local function parse_url(oil_url) local scheme, url = util.parse_url(oil_url) - assert(scheme and url, string.format("Malformed input url '%s'", oil_url)) local ret = { scheme = scheme } local username, rem = url:match("^([^@%s]+)@(.*)$") ret.user = username @@ -50,7 +42,6 @@ M.parse_url = function(oil_url) error(string.format("Malformed SSH url: %s", oil_url)) end - ---@cast ret oil.sshUrl return ret end @@ -89,19 +80,11 @@ local function url_to_scp(url) return table.concat(pieces, "") end ----@param url1 oil.sshUrl ----@param url2 oil.sshUrl ----@return boolean -local function url_hosts_equal(url1, url2) - return url1.host == url2.host and url1.port == url2.port and url1.user == url2.user -end - local _connections = {} ---@param url string ---@param allow_retry nil|boolean ----@return oil.sshFs local function get_connection(url, allow_retry) - local res = M.parse_url(url) + local res = parse_url(url) res.scheme = config.adapter_to_scheme.ssh res.path = "" local key = url_to_str(res) @@ -117,7 +100,7 @@ local ssh_columns = {} ssh_columns.permissions = { render = function(entry, conf) local meta = entry[FIELD_META] - return meta and permissions.mode_to_str(meta.mode) + return permissions.mode_to_str(meta.mode) end, parse = function(line, conf) @@ -126,7 +109,7 @@ ssh_columns.permissions = { compare = function(entry, parsed_value) local meta = entry[FIELD_META] - if parsed_value and meta and meta.mode then + if parsed_value and meta.mode then local mask = bit.lshift(1, 12) - 1 local old_mode = bit.band(meta.mode, mask) if parsed_value ~= old_mode then @@ -141,7 +124,7 @@ ssh_columns.permissions = { end, perform_action = function(action, callback) - local res = M.parse_url(action.url) + local res = parse_url(action.url) local conn = get_connection(action.url) conn:chmod(action.value, res.path, callback) end, @@ -150,7 +133,7 @@ ssh_columns.permissions = { ssh_columns.size = { render = function(entry, conf) local meta = entry[FIELD_META] - if not meta or not meta.size then + if not meta.size then return "" elseif meta.size >= 1e9 then return string.format("%.1fG", meta.size / 1e9) @@ -166,15 +149,6 @@ ssh_columns.size = { parse = function(line, conf) return line:match("^(%d+%S*)%s+(.*)$") end, - - get_sort_value = function(entry) - local meta = entry[FIELD_META] - if meta and meta.size then - return meta.size - else - return 0 - end - end, } ---@param name string @@ -194,7 +168,7 @@ end ---@param bufname string ---@return string M.get_parent = function(bufname) - local res = M.parse_url(bufname) + local res = parse_url(bufname) res.path = pathutil.parent(res.path) return url_to_str(res) end @@ -202,7 +176,7 @@ end ---@param url string ---@param callback fun(url: string) M.normalize_url = function(url, callback) - local res = M.parse_url(url) + local res = parse_url(url) local conn = get_connection(url, true) local path = res.path @@ -223,12 +197,18 @@ end ---@param url string ---@param column_defs string[] ----@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) +---@param callback fun(err: nil|string, entries: nil|oil.InternalEntry[]) M.list = function(url, column_defs, callback) - local res = M.parse_url(url) + local res = parse_url(url) + cache.begin_update_url(url) local conn = get_connection(url) - conn:list_dir(url, res.path, callback) + conn:list_dir(url, res.path, function(err, data) + if err or not data then + cache.end_update_url(url) + end + callback(err, data) + end) end ---@param bufnr integer @@ -272,11 +252,9 @@ M.render_action = function(action) local dest = action.dest_url if config.get_adapter_by_scheme(src) == M then local _, path = util.parse_url(dest) - assert(path) dest = files.to_short_os_path(path, action.entry_type) else local _, path = util.parse_url(src) - assert(path) src = files.to_short_os_path(path, action.entry_type) end return string.format(" %s %s -> %s", action.type:upper(), src, dest) @@ -289,7 +267,7 @@ end ---@param cb fun(err: nil|string) M.perform_action = function(action, cb) if action.type == "create" then - local res = M.parse_url(action.url) + local res = parse_url(action.url) local conn = get_connection(action.url) if action.entry_type == "directory" then conn:mkdir(res.path, cb) @@ -299,19 +277,19 @@ M.perform_action = function(action, cb) conn:touch(res.path, cb) end elseif action.type == "delete" then - local res = M.parse_url(action.url) + local res = parse_url(action.url) local conn = get_connection(action.url) conn:rm(res.path, cb) elseif action.type == "move" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) + local src_adapter = config.get_adapter_by_scheme(action.src_url) + local dest_adapter = config.get_adapter_by_scheme(action.dest_url) if src_adapter == M and dest_adapter == M then - local src_res = M.parse_url(action.src_url) - local dest_res = M.parse_url(action.dest_url) + local src_res = parse_url(action.src_url) + local dest_res = parse_url(action.dest_url) local src_conn = get_connection(action.src_url) local dest_conn = get_connection(action.dest_url) if src_conn ~= dest_conn then - scp({ "-r", url_to_scp(src_res), url_to_scp(dest_res) }, function(err) + shell.run({ "scp", "-C", "-r", url_to_scp(src_res), url_to_scp(dest_res) }, function(err) if err then return cb(err) end @@ -324,58 +302,52 @@ M.perform_action = function(action, cb) cb("We should never attempt to move across adapters") end elseif action.type == "copy" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) + local src_adapter = config.get_adapter_by_scheme(action.src_url) + local dest_adapter = config.get_adapter_by_scheme(action.dest_url) if src_adapter == M and dest_adapter == M then - local src_res = M.parse_url(action.src_url) - local dest_res = M.parse_url(action.dest_url) - if not url_hosts_equal(src_res, dest_res) then - scp({ "-r", url_to_scp(src_res), url_to_scp(dest_res) }, cb) - else - local src_conn = get_connection(action.src_url) - src_conn:cp(src_res.path, dest_res.path, cb) + local src_res = parse_url(action.src_url) + local dest_res = parse_url(action.dest_url) + local src_conn = get_connection(action.src_url) + local dest_conn = get_connection(action.dest_url) + if src_conn.host ~= dest_conn.host then + shell.run({ "scp", "-C", "-r", url_to_scp(src_res), url_to_scp(dest_res) }, cb) end + src_conn:cp(src_res.path, dest_res.path, cb) else local src_arg local dest_arg if src_adapter == M then - src_arg = url_to_scp(M.parse_url(action.src_url)) + src_arg = url_to_scp(parse_url(action.src_url)) local _, path = util.parse_url(action.dest_url) - assert(path) dest_arg = fs.posix_to_os_path(path) else local _, path = util.parse_url(action.src_url) - assert(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(parse_url(action.dest_url)) end - scp({ "-r", src_arg, dest_arg }, cb) + shell.run({ "scp", "-C", "-r", src_arg, dest_arg }, cb) end else cb(string.format("Bad action type: %s", action.type)) end end -M.supported_cross_adapter_actions = { files = "copy" } +M.supports_xfer = { files = true } ---@param bufnr integer M.read_file = function(bufnr) loading.set_loading(bufnr, true) local bufname = vim.api.nvim_buf_get_name(bufnr) - local url = M.parse_url(bufname) + local url = parse_url(bufname) local scp_url = url_to_scp(url) local basename = pathutil.basename(bufname) - local cache_dir = vim.fn.stdpath("cache") - assert(type(cache_dir) == "string") - local tmpdir = fs.join(cache_dir, "oil") + local tmpdir = fs.join(vim.fn.stdpath("cache"), "oil") fs.mkdirp(tmpdir) local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXX")) - if fd then - vim.loop.fs_close(fd) - end + vim.loop.fs_close(fd) local tmp_bufnr = vim.fn.bufadd(tmpfile) - scp({ scp_url, tmpfile }, function(err) + shell.run({ "scp", "-C", scp_url, tmpfile }, function(err) loading.set_loading(bufnr, false) vim.bo[bufnr].modifiable = true vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { silent = true } }) @@ -390,85 +362,35 @@ M.read_file = function(bufnr) vim.api.nvim_buf_set_lines(bufnr, 0, 1, true, {}) end vim.bo[bufnr].modified = false - local filetype = vim.filetype.match({ buf = bufnr, filename = basename }) - if filetype then - vim.bo[bufnr].filetype = filetype - end + vim.bo[bufnr].filetype = vim.filetype.match({ buf = bufnr, filename = basename }) vim.cmd.doautocmd({ args = { "BufReadPost", bufname }, mods = { silent = true } }) vim.api.nvim_buf_delete(tmp_bufnr, { force = true }) - vim.keymap.set("n", "gf", M.goto_file, { buffer = bufnr }) end) end ---@param bufnr integer M.write_file = function(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr) - local url = M.parse_url(bufname) - local scp_url = url_to_scp(url) - local cache_dir = vim.fn.stdpath("cache") - assert(type(cache_dir) == "string") - local tmpdir = fs.join(cache_dir, "oil") - local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXXXX")) - if fd then - vim.loop.fs_close(fd) - end - vim.cmd.doautocmd({ args = { "BufWritePre", bufname }, mods = { silent = true } }) vim.bo[bufnr].modifiable = false - vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } }) + local url = parse_url(bufname) + local scp_url = url_to_scp(url) + local tmpdir = fs.join(vim.fn.stdpath("cache"), "oil") + local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXXXX")) + vim.loop.fs_close(fd) + vim.cmd.doautocmd({ args = { "BufWritePre", bufname }, mods = { silent = true } }) + vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true } }) local tmp_bufnr = vim.fn.bufadd(tmpfile) - scp({ tmpfile, scp_url }, function(err) - vim.bo[bufnr].modifiable = true + shell.run({ "scp", "-C", tmpfile, scp_url }, function(err) if err then vim.notify(string.format("Error writing file: %s", err), vim.log.levels.ERROR) - else - vim.bo[bufnr].modified = false - vim.cmd.doautocmd({ args = { "BufWritePost", bufname }, mods = { silent = true } }) end + vim.bo[bufnr].modifiable = true + vim.bo[bufnr].modified = false + vim.cmd.doautocmd({ args = { "BufWritePost", bufname }, mods = { silent = true } }) vim.loop.fs_unlink(tmpfile) vim.api.nvim_buf_delete(tmp_bufnr, { force = true }) end) end -M.goto_file = function() - local url = M.parse_url(vim.api.nvim_buf_get_name(0)) - local fname = vim.fn.expand("") - local fullpath = fname - if not fs.is_absolute(fname) then - local pardir = vim.fs.dirname(url.path) - fullpath = fs.join(pardir, fname) - end - url.path = vim.fs.dirname(fullpath) - local parurl = url_to_str(url) - - ---@cast M oil.Adapter - util.adapter_list_all(M, parurl, {}, function(err, entries) - if err then - vim.notify(string.format("Error finding file '%s': %s", fname, err), vim.log.levels.ERROR) - return - end - assert(entries) - local name_map = {} - for _, entry in ipairs(entries) do - name_map[entry[FIELD_NAME]] = entry - end - - local basename = vim.fs.basename(fullpath) - if name_map[basename] then - url.path = fullpath - vim.cmd.edit({ args = { url_to_str(url) } }) - return - end - for suffix in vim.gsplit(vim.o.suffixesadd, ",", { plain = true, trimempty = true }) do - local suffixname = basename .. suffix - if name_map[suffixname] then - url.path = fullpath .. suffix - vim.cmd.edit({ args = { url_to_str(url) } }) - return - end - end - vim.notify(string.format("Can't find file '%s'", fname), vim.log.levels.ERROR) - end) -end - return M diff --git a/lua/oil/adapters/ssh/connection.lua b/lua/oil/adapters/ssh/connection.lua index 6a47c07..a106ecf 100644 --- a/lua/oil/adapters/ssh/connection.lua +++ b/lua/oil/adapters/ssh/connection.lua @@ -1,23 +1,5 @@ -local config = require("oil.config") local layout = require("oil.layout") local util = require("oil.util") - ----@class (exact) oil.sshCommand ----@field cmd string|string[] ----@field cb fun(err?: string, output?: string[]) ----@field running? boolean - ----@class (exact) oil.sshConnection ----@field new fun(url: oil.sshUrl): oil.sshConnection ----@field create_ssh_command fun(url: oil.sshUrl): string[] ----@field meta {user?: string, groups?: string[]} ----@field connection_error nil|string ----@field connected boolean ----@field private term_bufnr integer ----@field private jid integer ----@field private term_winid nil|integer ----@field private commands oil.sshCommand[] ----@field private _stdout string[] local SSHConnection = {} local function output_extend(agg, output) @@ -62,8 +44,7 @@ local function get_last_lines(bufnr, num_lines) end ---@param url oil.sshUrl ----@return string[] -function SSHConnection.create_ssh_command(url) +function SSHConnection.new(url) local host = url.host if url.user then host = url.user .. "@" .. host @@ -71,48 +52,39 @@ function SSHConnection.create_ssh_command(url) local command = { "ssh", host, - } - if url.port then - table.insert(command, "-p") - table.insert(command, url.port) - end - return command -end - ----@param url oil.sshUrl ----@return oil.sshConnection -function SSHConnection.new(url) - local command = SSHConnection.create_ssh_command(url) - vim.list_extend(command, { - "/bin/sh", + "/bin/bash", + "--norc", "-c", -- 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. - "echo '_make_newline_'; echo '===READY==='; exec /bin/sh", - }) - local term_bufnr = vim.api.nvim_create_buf(false, true) + "echo '_make_newline_'; echo '===READY==='; exec /bin/bash --norc", + } + if url.port then + table.insert(command, 2, "-p") + table.insert(command, 3, url.port) + end local self = setmetatable({ + host = host, meta = {}, commands = {}, connected = false, connection_error = nil, - term_bufnr = term_bufnr, }, { __index = SSHConnection, }) + self.term_bufnr = vim.api.nvim_create_buf(false, true) local term_id local mode = vim.api.nvim_get_mode().mode - util.run_in_fullscreen_win(term_bufnr, function() - term_id = vim.api.nvim_open_term(term_bufnr, { + util.run_in_fullscreen_win(self.term_bufnr, function() + term_id = vim.api.nvim_open_term(self.term_bufnr, { on_input = function(_, _, _, data) - ---@diagnostic disable-next-line: invisible pcall(vim.api.nvim_chan_send, self.jid, data) end, }) end) 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", host)) util.hack_around_termopen_autocmd(mode) -- If it takes more than 2 seconds to connect, pop open the terminal @@ -126,7 +98,6 @@ function SSHConnection.new(url) pty = true, -- This is require for interactivity on_stdout = function(j, output) pcall(vim.api.nvim_chan_send, self.term_id, table.concat(output, "\r\n")) - ---@diagnostic disable-next-line: invisible local new_i_start = output_extend(self._stdout, output) self:_handle_output(new_i_start) end, @@ -156,27 +127,24 @@ function SSHConnection.new(url) else self.jid = jid end - self:run("id -u", function(err, lines) + self:run("whoami", function(err, lines) if err then vim.notify(string.format("Error fetching ssh connection user: %s", err), vim.log.levels.WARN) else - assert(lines) self.meta.user = vim.trim(table.concat(lines, "")) end end) - self:run("id -G", function(err, lines) + self:run("groups", function(err, lines) if err then vim.notify( string.format("Error fetching ssh connection user groups: %s", err), vim.log.levels.WARN ) else - assert(lines) self.meta.groups = vim.split(table.concat(lines, ""), "%s+", { trimempty = true }) end end) - ---@cast self oil.sshConnection return self end @@ -213,7 +181,6 @@ function SSHConnection:_handle_output(start_i) end else for i = start_i, #self._stdout - 1 do - ---@type string local line = self._stdout[i] if line:match("^===BEGIN===%s*$") then self._stdout = util.tbl_slice(self._stdout, i + 1) @@ -279,7 +246,7 @@ function SSHConnection:open_terminal() row = row, col = col, style = "minimal", - border = config.ssh.border, + border = "rounded", }) vim.cmd.startinsert() end @@ -305,9 +272,7 @@ function SSHConnection:_consume() -- HACK: Sleep briefly to help reduce stderr/stdout interleaving. -- I want to find a way to flush the stderr before the echo DONE, but haven't yet. -- This was causing issues when ls directory that doesn't exist (b/c ls prints error) - 'echo "===BEGIN==="; ' - .. cmd.cmd - .. '; CODE=$?; sleep .01; echo "===DONE($CODE)==="\r' + 'echo "===BEGIN==="; ' .. cmd.cmd .. '; CODE=$?; sleep .01; echo "===DONE($CODE)==="\r' ) end end diff --git a/lua/oil/adapters/ssh/sshfs.lua b/lua/oil/adapters/ssh/sshfs.lua index 0dcc169..47eea01 100644 --- a/lua/oil/adapters/ssh/sshfs.lua +++ b/lua/oil/adapters/ssh/sshfs.lua @@ -1,12 +1,8 @@ -local SSHConnection = require("oil.adapters.ssh.connection") local cache = require("oil.cache") local constants = require("oil.constants") local permissions = require("oil.adapters.files.permissions") +local SSHConnection = require("oil.adapters.ssh.connection") local util = require("oil.util") - ----@class (exact) oil.sshFs ----@field new fun(url: oil.sshUrl): oil.sshFs ----@field conn oil.sshConnection local SSHFS = {} local FIELD_TYPE = constants.FIELD_TYPE @@ -24,10 +20,10 @@ local typechar_map = { ---@param line string ---@return string Name of entry ---@return oil.EntryType ----@return table Metadata for entry +---@return nil|table Metadata for entry local function parse_ls_line(line) local typechar, perms, refcount, user, group, rem = - line:match("^(.)(%S+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(.*)$") + line:match("^(.)(%S+)%s+(%d+)%s+(%S+)%s+(%S+)%s+(.*)$") if not typechar then error(string.format("Could not parse '%s'", line)) end @@ -42,17 +38,10 @@ local function parse_ls_line(line) local name, size, date, major, minor if typechar == "c" or typechar == "b" then major, minor, date, name = rem:match("^(%d+)%s*,%s*(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)") - if name == nil then - major, minor, date, name = - rem:match("^(%d+)%s*,%s*(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)") - end meta.major = tonumber(major) meta.minor = tonumber(minor) else size, date, name = rem:match("^(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)") - if name == nil then - size, date, name = rem:match("^(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)") - end meta.size = tonumber(size) end meta.iso_modified_date = date @@ -68,16 +57,8 @@ local function parse_ls_line(line) return name, type, meta end ----@param str string String to escape ----@return string Escaped string -local function shellescape(str) - return "'" .. str:gsub("'", "'\\''") .. "'" -end - ---@param url oil.sshUrl ----@return oil.sshFs function SSHFS.new(url) - ---@type oil.sshFs return setmetatable({ conn = SSHConnection.new(url), }, { @@ -94,7 +75,7 @@ end ---@param callback fun(err: nil|string) function SSHFS:chmod(value, path, callback) 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, path), callback) end function SSHFS:open_terminal() @@ -113,44 +94,36 @@ function SSHFS:realpath(path, callback) if err then return callback(err) end - assert(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 vim.endswith(abspath, ".") then abspath = abspath:sub(1, #abspath - 1) end - self.conn:run( - string.format("LC_ALL=C ls -land --color=never %s", shellescape(abspath)), - function(ls_err, ls_lines) - local type - if ls_err then - -- If the file doesn't exist, treat it like a not-yet-existing directory - type = "directory" - else - assert(ls_lines) - local _ - _, type = parse_ls_line(ls_lines[1]) - end - if type == "directory" then - abspath = util.addslash(abspath) - end - callback(nil, abspath) + self.conn:run(string.format("ls -fld '%s'", abspath), function(ls_err, ls_lines) + local type + if ls_err then + -- If the file doesn't exist, treat it like a not-yet-existing directory + type = "directory" + else + local _ + _, type = parse_ls_line(ls_lines[1]) end - ) + if type == "directory" then + abspath = util.addslash(abspath) + end + callback(nil, abspath) + end) end) end local dir_meta = {} ----@param url string ----@param path string ----@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) function SSHFS:list_dir(url, path, callback) local path_postfix = "" if path ~= "" then - path_postfix = string.format(" %s", shellescape(path)) + path_postfix = string.format(" '%s'", path) end - self.conn:run("LC_ALL=C ls -lan --color=never" .. path_postfix, function(err, lines) + self.conn:run("LANG=C ls -fl" .. path_postfix, function(err, lines) if err 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 @@ -160,10 +133,8 @@ function SSHFS:list_dir(url, path, callback) return callback(err) end end - assert(lines) local any_links = false local entries = {} - local cache_entries = {} for _, line in ipairs(lines) do if line ~= "" and not line:match("^total") then local name, type, meta = parse_ls_line(line) @@ -174,42 +145,38 @@ function SSHFS:list_dir(url, path, callback) any_links = true end local cache_entry = cache.create_entry(url, name, type) - table.insert(cache_entries, cache_entry) entries[name] = cache_entry cache_entry[FIELD_META] = meta + cache.store_entry(url, cache_entry) end end end if any_links then -- 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 - self.conn:run( - "LC_ALL=C ls -naLl --color=never" .. path_postfix .. " 2> /dev/null", - function(link_err, link_lines) - -- 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 - return callback(link_err) - end - assert(link_lines) - for _, line in ipairs(link_lines) do - if line ~= "" and not line:match("^total") then - local ok, name, type, meta = pcall(parse_ls_line, line) - if ok and name ~= "." and name ~= ".." then - local cache_entry = entries[name] - if cache_entry[FIELD_TYPE] == "link" then - cache_entry[FIELD_META].link_stat = { - type = type, - size = meta.size, - } - end + self.conn:run("ls -fLl" .. path_postfix, function(link_err, link_lines) + -- 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 + return callback(link_err) + end + for _, line in ipairs(link_lines) do + if line ~= "" and not line:match("^total") then + local ok, name, type, meta = pcall(parse_ls_line, line) + if ok and name ~= "." and name ~= ".." then + local cache_entry = entries[name] + if cache_entry[FIELD_TYPE] == "link" then + cache_entry[FIELD_META].link_stat = { + type = type, + size = meta.size, + } end end end - callback(nil, cache_entries) end - ) + callback() + end) else - callback(nil, cache_entries) + callback() end end) end @@ -217,40 +184,40 @@ end ---@param path string ---@param callback fun(err: nil|string) function SSHFS:mkdir(path, callback) - self.conn:run(string.format("mkdir -p %s", shellescape(path)), callback) + self.conn:run(string.format("mkdir -p '%s'", path), callback) end ---@param path string ---@param callback fun(err: nil|string) function SSHFS:touch(path, callback) - self.conn:run(string.format("touch %s", shellescape(path)), callback) + self.conn:run(string.format("touch '%s'", path), callback) end ---@param path string ---@param link string ---@param callback fun(err: nil|string) 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'", link, path), callback) end ---@param path string ---@param callback fun(err: nil|string) function SSHFS:rm(path, callback) - self.conn:run(string.format("rm -rf %s", shellescape(path)), callback) + self.conn:run(string.format("rm -rf '%s'", path), callback) end ---@param src string ---@param dest string ---@param callback fun(err: nil|string) 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'", src, dest), callback) end ---@param src string ---@param dest string ---@param callback fun(err: nil|string) 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'", src, dest), callback) end function SSHFS:get_dir_meta(url) diff --git a/lua/oil/adapters/test.lua b/lua/oil/adapters/test.lua index c4176ef..b127dfc 100644 --- a/lua/oil/adapters/test.lua +++ b/lua/oil/adapters/test.lua @@ -1,5 +1,4 @@ local cache = require("oil.cache") -local util = require("oil.util") local M = {} ---@param url string @@ -8,49 +7,11 @@ M.normalize_url = function(url, callback) callback(url) end -local dir_listing = {} - ----@param url string ----@param column_defs string[] ----@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) -M.list = function(url, column_defs, cb) - local _, path = util.parse_url(url) - local entries = dir_listing[path] or {} - local cache_entries = {} - for _, entry in ipairs(entries) do - local cache_entry = cache.create_entry(url, entry.name, entry.entry_type) - table.insert(cache_entries, cache_entry) - end - cb(nil, cache_entries) -end - -M.test_clear = function() - dir_listing = {} -end - ---@param path string ----@param entry_type oil.EntryType ----@return oil.InternalEntry -M.test_set = function(path, entry_type) - if path == "/" then - return {} - end - local parent = vim.fn.fnamemodify(path, ":h") - if parent ~= path then - M.test_set(parent, "directory") - end - parent = util.addslash(parent) - if not dir_listing[parent] then - dir_listing[parent] = {} - end - local name = vim.fn.fnamemodify(path, ":t") - local entry = { - name = name, - entry_type = entry_type, - } - table.insert(dir_listing[parent], entry) - local parent_url = "oil-test://" .. parent - return cache.create_and_store_entry(parent_url, entry.name, entry.entry_type) +---@param column_defs string[] +---@param cb fun(err: nil|string, entries: nil|oil.InternalEntry[]) +M.list = function(url, column_defs, cb) + cb(nil, cache.list_url(url)) end ---@param name string @@ -59,6 +20,22 @@ M.get_column = function(name) return nil end +---@param path string +---@param entry_type oil.EntryType +M.test_set = function(path, entry_type) + local parent = vim.fn.fnamemodify(path, ":h") + if parent ~= path then + M.test_set(parent, "directory") + end + local url = "oil-test://" .. path + if cache.get_entry_by_url(url) then + -- Already exists + return + end + local name = vim.fn.fnamemodify(path, ":t") + cache.create_and_store_entry("oil-test://" .. parent, name, entry_type) +end + ---@param bufnr integer ---@return boolean M.is_modifiable = function(bufnr) diff --git a/lua/oil/adapters/trash.lua b/lua/oil/adapters/trash.lua deleted file mode 100644 index b007074..0000000 --- a/lua/oil/adapters/trash.lua +++ /dev/null @@ -1,9 +0,0 @@ -local fs = require("oil.fs") - -if fs.is_mac then - return require("oil.adapters.trash.mac") -elseif fs.is_windows then - return require("oil.adapters.trash.windows") -else - return require("oil.adapters.trash.freedesktop") -end diff --git a/lua/oil/adapters/trash/freedesktop.lua b/lua/oil/adapters/trash/freedesktop.lua deleted file mode 100644 index 10c0749..0000000 --- a/lua/oil/adapters/trash/freedesktop.lua +++ /dev/null @@ -1,633 +0,0 @@ --- Based on the FreeDesktop.org trash specification --- https://specifications.freedesktop.org/trash-spec/1.0/ -local cache = require("oil.cache") -local config = require("oil.config") -local constants = require("oil.constants") -local files = require("oil.adapters.files") -local fs = require("oil.fs") -local util = require("oil.util") - -local uv = vim.uv or vim.loop -local FIELD_META = constants.FIELD_META - -local M = {} - -local function ensure_trash_dir(path) - local mode = 448 -- 0700 - fs.mkdirp(fs.join(path, "info"), mode) - fs.mkdirp(fs.join(path, "files"), mode) -end - ----Gets the location of the home trash dir, creating it if necessary ----@return string -local function get_home_trash_dir() - local xdg_home = vim.env.XDG_DATA_HOME - if not xdg_home then - xdg_home = fs.join(assert(uv.os_homedir()), ".local", "share") - end - local trash_dir = fs.join(xdg_home, "Trash") - ensure_trash_dir(trash_dir) - return trash_dir -end - ----@param mode integer ----@return boolean -local function is_sticky(mode) - local extra = bit.rshift(mode, 9) - return bit.band(extra, 4) ~= 0 -end - ----Get the topdir .Trash/$uid directory if present and valid ----@param path string ----@return string[] -local function get_top_trash_dirs(path) - local dirs = {} - local dev = (uv.fs_lstat(path) or {}).dev - local top_trash_dirs = vim.fs.find(".Trash", { upward = true, path = path, limit = math.huge }) - for _, top_trash_dir in ipairs(top_trash_dirs) do - local stat = uv.fs_lstat(top_trash_dir) - if stat and not dev then - dev = stat.dev - end - if stat and stat.dev == dev and stat.type == "directory" and is_sticky(stat.mode) then - local trash_dir = fs.join(top_trash_dir, tostring(uv.getuid())) - ensure_trash_dir(trash_dir) - table.insert(dirs, trash_dir) - end - end - - -- Also search for the .Trash-$uid - top_trash_dirs = vim.fs.find( - string.format(".Trash-%d", uv.getuid()), - { upward = true, path = path, limit = math.huge } - ) - for _, top_trash_dir in ipairs(top_trash_dirs) do - local stat = uv.fs_lstat(top_trash_dir) - if stat and stat.dev == dev then - ensure_trash_dir(top_trash_dir) - table.insert(dirs, top_trash_dir) - end - end - - return dirs -end - ----@param path string ----@return string -local function get_write_trash_dir(path) - local lstat = uv.fs_lstat(path) - local home_trash = get_home_trash_dir() - if not lstat then - -- If the source file doesn't exist default to home trash dir - return home_trash - end - local dev = lstat.dev - if uv.fs_lstat(home_trash).dev == dev then - return home_trash - end - - local top_trash_dirs = get_top_trash_dirs(path) - if not vim.tbl_isempty(top_trash_dirs) then - return top_trash_dirs[1] - end - - local parent = vim.fn.fnamemodify(path, ":h") - local next_parent = vim.fn.fnamemodify(parent, ":h") - while parent ~= next_parent and uv.fs_lstat(next_parent).dev == dev do - parent = next_parent - next_parent = vim.fn.fnamemodify(parent, ":h") - end - - local top_trash = fs.join(parent, string.format(".Trash-%d", uv.getuid())) - ensure_trash_dir(top_trash) - return top_trash -end - ----@param path string ----@return string[] -local function get_read_trash_dirs(path) - local dirs = { get_home_trash_dir() } - vim.list_extend(dirs, get_top_trash_dirs(path)) - return dirs -end - ----@param url string ----@param callback fun(url: string) -M.normalize_url = function(url, callback) - local scheme, path = util.parse_url(url) - assert(path) - local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p") - uv.fs_realpath( - os_path, - vim.schedule_wrap(function(err, new_os_path) - local realpath = new_os_path or os_path - callback(scheme .. util.addslash(fs.os_to_posix_path(realpath))) - end) - ) -end - ----@param url string ----@param entry oil.Entry ----@param cb fun(path: string) -M.get_entry_path = function(url, entry, cb) - local internal_entry = assert(cache.get_entry_by_id(entry.id)) - local meta = assert(internal_entry[FIELD_META]) - ---@type oil.TrashInfo - local trash_info = meta.trash_info - if not trash_info then - -- This is a subpath in the trash - M.normalize_url(url, cb) - return - end - local path = fs.os_to_posix_path(trash_info.trash_file) - if meta.stat.type == "directory" then - path = util.addslash(path) - end - cb("oil://" .. path) -end - ----@class oil.TrashInfo ----@field trash_file string ----@field info_file string ----@field original_path string ----@field deletion_date number ----@field stat uv.aliases.fs_stat_table - ----@param info_file string ----@param cb fun(err?: string, info?: oil.TrashInfo) -local function read_trash_info(info_file, cb) - if not vim.endswith(info_file, ".trashinfo") then - return cb("File is not .trashinfo") - end - uv.fs_open(info_file, "r", 448, function(err, fd) - if err then - return cb(err) - end - assert(fd) - uv.fs_fstat(fd, function(stat_err, stat) - if stat_err then - uv.fs_close(fd) - return cb(stat_err) - end - uv.fs_read( - fd, - assert(stat).size, - nil, - vim.schedule_wrap(function(read_err, content) - uv.fs_close(fd) - if read_err then - return cb(read_err) - end - assert(content) - local trash_info = { - info_file = info_file, - } - local lines = vim.split(content, "\r?\n") - if lines[1] ~= "[Trash Info]" then - return cb("File missing [Trash Info] header") - end - local trash_base = vim.fn.fnamemodify(info_file, ":h:h") - for _, line in ipairs(lines) do - local key, value = unpack(vim.split(line, "=", { plain = true, trimempty = true })) - if key == "Path" and not trash_info.original_path then - if not vim.startswith(value, "/") then - value = fs.join(trash_base, value) - end - trash_info.original_path = value - elseif key == "DeletionDate" and not trash_info.deletion_date then - trash_info.deletion_date = vim.fn.strptime("%Y-%m-%dT%H:%M:%S", value) - end - end - - if not trash_info.original_path or not trash_info.deletion_date then - return cb("File missing required fields") - end - - local basename = vim.fn.fnamemodify(info_file, ":t:r") - trash_info.trash_file = fs.join(trash_base, "files", basename) - uv.fs_lstat(trash_info.trash_file, function(trash_stat_err, trash_stat) - if trash_stat_err then - cb(".trashinfo file points to non-existant file") - else - trash_info.stat = trash_stat - ---@cast trash_info oil.TrashInfo - cb(nil, trash_info) - end - end) - end) - ) - end) - end) -end - ----@param url string ----@param column_defs string[] ----@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) -M.list = function(url, column_defs, cb) - cb = vim.schedule_wrap(cb) - local _, path = util.parse_url(url) - assert(path) - local trash_dirs = get_read_trash_dirs(path) - local trash_idx = 0 - - local read_next_trash_dir - read_next_trash_dir = function() - trash_idx = trash_idx + 1 - local trash_dir = trash_dirs[trash_idx] - if not trash_dir then - return cb() - end - - -- Show all files from the trash directory if we are in the root of the device, which we can - -- tell if the trash dir is a subpath of our current path - local show_all_files = fs.is_subpath(path, trash_dir) - -- The first trash dir is a special case; it is in the home directory and we should only show - -- all entries if we are in the top root path "/" - if trash_idx == 1 then - show_all_files = path == "/" - end - - local info_dir = fs.join(trash_dir, "info") - ---@diagnostic disable-next-line: param-type-mismatch, discard-returns - uv.fs_opendir(info_dir, function(open_err, fd) - if open_err then - if open_err:match("^ENOENT: no such file or directory") then - -- If the directory doesn't exist, treat the list as a success. We will be able to traverse - -- and edit a not-yet-existing directory. - return read_next_trash_dir() - else - return cb(open_err) - end - end - local read_next - read_next = function() - uv.fs_readdir(fd, function(err, entries) - if err then - uv.fs_closedir(fd, function() - cb(err) - end) - return - elseif entries then - local internal_entries = {} - local poll = util.cb_collect(#entries, function(inner_err) - if inner_err then - cb(inner_err) - else - cb(nil, internal_entries, read_next) - end - end) - - for _, entry in ipairs(entries) do - read_trash_info( - fs.join(info_dir, entry.name), - vim.schedule_wrap(function(read_err, info) - if read_err then - -- Discard the error. We don't care if there's something wrong with one of these - -- files. - poll() - else - local parent = util.addslash(vim.fn.fnamemodify(info.original_path, ":h")) - if path == parent or show_all_files then - local name = vim.fn.fnamemodify(info.trash_file, ":t") - ---@diagnostic disable-next-line: undefined-field - local cache_entry = cache.create_entry(url, name, info.stat.type) - local display_name = vim.fn.fnamemodify(info.original_path, ":t") - cache_entry[FIELD_META] = { - stat = info.stat, - trash_info = info, - display_name = display_name, - } - table.insert(internal_entries, cache_entry) - end - if path ~= parent and (show_all_files or fs.is_subpath(path, parent)) then - local name = parent:sub(path:len() + 1) - local next_par = vim.fs.dirname(name) - while next_par ~= "." do - name = next_par - next_par = vim.fs.dirname(name) - end - ---@diagnostic disable-next-line: undefined-field - local cache_entry = cache.create_entry(url, name, "directory") - - cache_entry[FIELD_META] = { - stat = info.stat, - } - table.insert(internal_entries, cache_entry) - end - poll() - end - end) - ) - end - else - uv.fs_closedir(fd, function(close_err) - if close_err then - cb(close_err) - else - vim.schedule(read_next_trash_dir) - end - end) - end - end) - end - read_next() - ---@diagnostic disable-next-line: param-type-mismatch - end, 10000) - end - read_next_trash_dir() -end - ----@param bufnr integer ----@return boolean -M.is_modifiable = function(bufnr) - return true -end - -local file_columns = {} - -local current_year --- Make sure we run this import-time effect in the main loop (mostly for tests) -vim.schedule(function() - current_year = vim.fn.strftime("%Y") -end) - -file_columns.mtime = { - render = function(entry, conf) - local meta = entry[FIELD_META] - if not meta then - return nil - end - ---@type oil.TrashInfo - local trash_info = meta.trash_info - local time = trash_info and trash_info.deletion_date or meta.stat and meta.stat.mtime.sec - if not time then - return nil - end - local fmt = conf and conf.format - local ret - if fmt then - ret = vim.fn.strftime(fmt, time) - else - local year = vim.fn.strftime("%Y", time) - if year ~= current_year then - ret = vim.fn.strftime("%b %d %Y", time) - else - ret = vim.fn.strftime("%b %d %H:%M", time) - end - end - return ret - end, - - get_sort_value = function(entry) - local meta = entry[FIELD_META] - ---@type nil|oil.TrashInfo - local trash_info = meta and meta.trash_info - if trash_info then - return trash_info.deletion_date - else - return 0 - end - end, - - parse = function(line, conf) - local fmt = conf and conf.format - local pattern - if fmt then - pattern = fmt:gsub("%%.", "%%S+") - else - pattern = "%S+%s+%d+%s+%d%d:?%d%d" - end - return line:match("^(" .. pattern .. ")%s+(.+)$") - end, -} - ----@param name string ----@return nil|oil.ColumnDefinition -M.get_column = function(name) - return file_columns[name] -end - -M.supported_cross_adapter_actions = { files = "move" } - ----@param action oil.Action ----@return boolean -M.filter_action = function(action) - if action.type == "create" then - return false - elseif action.type == "delete" then - local entry = assert(cache.get_entry_by_url(action.url)) - local meta = entry[FIELD_META] - return meta ~= nil and meta.trash_info ~= nil - elseif action.type == "move" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - return src_adapter.name == "files" or dest_adapter.name == "files" - elseif action.type == "copy" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - return src_adapter.name == "files" or dest_adapter.name == "files" - else - error(string.format("Bad action type '%s'", action.type)) - end -end - ----@param err oil.ParseError ----@return boolean -M.filter_error = function(err) - if err.message == "Duplicate filename" then - return false - end - return true -end - ----@param action oil.Action ----@return string -M.render_action = function(action) - if action.type == "delete" then - local entry = assert(cache.get_entry_by_url(action.url)) - local meta = entry[FIELD_META] - ---@type oil.TrashInfo - local trash_info = assert(meta).trash_info - local short_path = fs.shorten_path(trash_info.original_path) - return string.format(" PURGE %s", short_path) - elseif action.type == "move" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - if src_adapter.name == "files" then - local _, path = util.parse_url(action.src_url) - assert(path) - local short_path = files.to_short_os_path(path, action.entry_type) - return string.format(" TRASH %s", short_path) - elseif dest_adapter.name == "files" then - local _, path = util.parse_url(action.dest_url) - assert(path) - local short_path = files.to_short_os_path(path, action.entry_type) - return string.format("RESTORE %s", short_path) - else - error("Must be moving files into or out of trash") - end - elseif action.type == "copy" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - if src_adapter.name == "files" then - local _, path = util.parse_url(action.src_url) - assert(path) - local short_path = files.to_short_os_path(path, action.entry_type) - return string.format(" COPY %s -> TRASH", short_path) - elseif dest_adapter.name == "files" then - local _, path = util.parse_url(action.dest_url) - assert(path) - local short_path = files.to_short_os_path(path, action.entry_type) - return string.format("RESTORE %s", short_path) - else - error("Must be copying files into or out of trash") - end - else - error(string.format("Bad action type '%s'", action.type)) - end -end - ----@param trash_info oil.TrashInfo ----@param cb fun(err?: string) -local function purge(trash_info, cb) - fs.recursive_delete("file", trash_info.info_file, function(err) - if err then - return cb(err) - end - ---@diagnostic disable-next-line: undefined-field - fs.recursive_delete(trash_info.stat.type, trash_info.trash_file, cb) - end) -end - ----@param path string ----@param info_path string ----@param cb fun(err?: string) -local function write_info_file(path, info_path, cb) - uv.fs_open( - info_path, - "w", - 448, - vim.schedule_wrap(function(err, fd) - if err then - return cb(err) - end - assert(fd) - local deletion_date = vim.fn.strftime("%Y-%m-%dT%H:%M:%S") - local contents = string.format("[Trash Info]\nPath=%s\nDeletionDate=%s", path, deletion_date) - uv.fs_write(fd, contents, function(write_err) - uv.fs_close(fd, function(close_err) - cb(write_err or close_err) - end) - end) - end) - ) -end - ----@param path string ----@param cb fun(err?: string, trash_info?: oil.TrashInfo) -local function create_trash_info(path, cb) - local trash_dir = get_write_trash_dir(path) - local basename = vim.fs.basename(path) - local now = os.time() - local name = string.format("%s-%d.%d", basename, now, math.random(100000, 999999)) - local dest_path = fs.join(trash_dir, "files", name) - local dest_info = fs.join(trash_dir, "info", name .. ".trashinfo") - uv.fs_lstat(path, function(err, stat) - if err then - return cb(err) - end - assert(stat) - write_info_file(path, dest_info, function(info_err) - if info_err then - return cb(info_err) - end - ---@type oil.TrashInfo - local trash_info = { - original_path = path, - trash_file = dest_path, - info_file = dest_info, - deletion_date = now, - stat = stat, - } - cb(nil, trash_info) - end) - end) -end - ----@param action oil.Action ----@param cb fun(err: nil|string) -M.perform_action = function(action, cb) - if action.type == "delete" then - local entry = assert(cache.get_entry_by_url(action.url)) - local meta = entry[FIELD_META] - ---@type oil.TrashInfo - local trash_info = assert(meta).trash_info - purge(trash_info, cb) - elseif action.type == "move" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - if src_adapter.name == "files" then - local _, path = util.parse_url(action.src_url) - M.delete_to_trash(assert(path), cb) - elseif dest_adapter.name == "files" then - -- Restore - local _, dest_path = util.parse_url(action.dest_url) - assert(dest_path) - local entry = assert(cache.get_entry_by_url(action.src_url)) - local meta = entry[FIELD_META] - ---@type oil.TrashInfo - local trash_info = assert(meta).trash_info - fs.recursive_move(action.entry_type, trash_info.trash_file, dest_path, function(err) - if err then - return cb(err) - end - uv.fs_unlink(trash_info.info_file, cb) - end) - else - error("Must be moving files into or out of trash") - end - elseif action.type == "copy" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - if src_adapter.name == "files" then - local _, path = util.parse_url(action.src_url) - assert(path) - create_trash_info(path, function(err, trash_info) - if err then - cb(err) - else - local stat_type = trash_info.stat.type or "unknown" - fs.recursive_copy(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb)) - end - end) - elseif dest_adapter.name == "files" then - -- Restore - local _, dest_path = util.parse_url(action.dest_url) - assert(dest_path) - local entry = assert(cache.get_entry_by_url(action.src_url)) - local meta = entry[FIELD_META] - ---@type oil.TrashInfo - local trash_info = assert(meta).trash_info - fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb) - else - error("Must be moving files into or out of trash") - end - else - cb(string.format("Bad action type: %s", action.type)) - end -end - ----@param path string ----@param cb fun(err?: string) -M.delete_to_trash = function(path, cb) - create_trash_info(path, function(err, trash_info) - if err then - cb(err) - else - local stat_type = trash_info.stat.type or "unknown" - fs.recursive_move(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb)) - end - end) -end - -return M diff --git a/lua/oil/adapters/trash/mac.lua b/lua/oil/adapters/trash/mac.lua deleted file mode 100644 index 66cf4c1..0000000 --- a/lua/oil/adapters/trash/mac.lua +++ /dev/null @@ -1,232 +0,0 @@ -local cache = require("oil.cache") -local config = require("oil.config") -local files = require("oil.adapters.files") -local fs = require("oil.fs") -local util = require("oil.util") - -local uv = vim.uv or vim.loop - -local M = {} - -local function touch_dir(path) - uv.fs_mkdir(path, 448) -- 0700 -end - ----Gets the location of the home trash dir, creating it if necessary ----@return string -local function get_trash_dir() - local trash_dir = fs.join(assert(uv.os_homedir()), ".Trash") - touch_dir(trash_dir) - return trash_dir -end - ----@param url string ----@param callback fun(url: string) -M.normalize_url = function(url, callback) - local scheme, path = util.parse_url(url) - assert(path) - callback(scheme .. "/") -end - ----@param url string ----@param entry oil.Entry ----@param cb fun(path: string) -M.get_entry_path = function(url, entry, cb) - local trash_dir = get_trash_dir() - local path = fs.join(trash_dir, entry.name) - if entry.type == "directory" then - path = "oil://" .. path - end - cb(path) -end - ----@param url string ----@param column_defs string[] ----@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) -M.list = function(url, column_defs, cb) - cb = vim.schedule_wrap(cb) - local _, path = util.parse_url(url) - assert(path) - local trash_dir = get_trash_dir() - ---@diagnostic disable-next-line: param-type-mismatch, discard-returns - uv.fs_opendir(trash_dir, function(open_err, fd) - if open_err then - if open_err:match("^ENOENT: no such file or directory") then - -- If the directory doesn't exist, treat the list as a success. We will be able to traverse - -- and edit a not-yet-existing directory. - return cb() - else - return cb(open_err) - end - end - local read_next - read_next = function() - uv.fs_readdir(fd, function(err, entries) - if err then - uv.fs_closedir(fd, function() - cb(err) - end) - return - elseif entries then - local internal_entries = {} - local poll = util.cb_collect(#entries, function(inner_err) - if inner_err then - cb(inner_err) - else - cb(nil, internal_entries, read_next) - end - end) - - for _, entry in ipairs(entries) do - -- TODO: read .DS_Store and filter by original dir - local cache_entry = cache.create_entry(url, entry.name, entry.type) - table.insert(internal_entries, cache_entry) - poll() - end - else - uv.fs_closedir(fd, function(close_err) - if close_err then - cb(close_err) - else - cb() - end - end) - end - end) - end - read_next() - ---@diagnostic disable-next-line: param-type-mismatch - end, 10000) -end - ----@param bufnr integer ----@return boolean -M.is_modifiable = function(bufnr) - return true -end - ----@param name string ----@return nil|oil.ColumnDefinition -M.get_column = function(name) - return nil -end - -M.supported_cross_adapter_actions = { files = "move" } - ----@param action oil.Action ----@return string -M.render_action = function(action) - if action.type == "create" then - return string.format("CREATE %s", action.url) - elseif action.type == "delete" then - return string.format(" PURGE %s", action.url) - elseif action.type == "move" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - if src_adapter.name == "files" then - local _, path = util.parse_url(action.src_url) - assert(path) - local short_path = files.to_short_os_path(path, action.entry_type) - return string.format(" TRASH %s", short_path) - elseif dest_adapter.name == "files" then - local _, path = util.parse_url(action.dest_url) - assert(path) - local short_path = files.to_short_os_path(path, action.entry_type) - return string.format("RESTORE %s", short_path) - else - return string.format(" %s %s -> %s", action.type:upper(), action.src_url, action.dest_url) - end - elseif action.type == "copy" then - return string.format(" %s %s -> %s", action.type:upper(), action.src_url, action.dest_url) - else - error("Bad action type") - end -end - ----@param action oil.Action ----@param cb fun(err: nil|string) -M.perform_action = function(action, cb) - local trash_dir = get_trash_dir() - if action.type == "create" then - local _, path = util.parse_url(action.url) - assert(path) - path = trash_dir .. path - if action.entry_type == "directory" then - uv.fs_mkdir(path, 493, function(err) - -- Ignore if the directory already exists - if not err or err:match("^EEXIST:") then - cb() - else - cb(err) - end - end) -- 0755 - elseif action.entry_type == "link" and action.link then - local flags = nil - local target = fs.posix_to_os_path(action.link) - ---@diagnostic disable-next-line: param-type-mismatch - uv.fs_symlink(target, path, flags, cb) - else - fs.touch(path, cb) - end - elseif action.type == "delete" then - local _, path = util.parse_url(action.url) - assert(path) - local fullpath = trash_dir .. path - fs.recursive_delete(action.entry_type, fullpath, cb) - elseif action.type == "move" or action.type == "copy" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - local _, src_path = util.parse_url(action.src_url) - local _, dest_path = util.parse_url(action.dest_url) - assert(src_path and dest_path) - if src_adapter.name == "files" then - dest_path = trash_dir .. dest_path - elseif dest_adapter.name == "files" then - src_path = trash_dir .. src_path - else - dest_path = trash_dir .. dest_path - src_path = trash_dir .. src_path - end - - if action.type == "move" then - fs.recursive_move(action.entry_type, src_path, dest_path, cb) - else - fs.recursive_copy(action.entry_type, src_path, dest_path, cb) - end - else - cb(string.format("Bad action type: %s", action.type)) - end -end - ----@param path string ----@param cb fun(err?: string) -M.delete_to_trash = function(path, cb) - local basename = vim.fs.basename(path) - local trash_dir = get_trash_dir() - local dest = fs.join(trash_dir, basename) - uv.fs_lstat( - path, - vim.schedule_wrap(function(stat_err, src_stat) - if stat_err then - return cb(stat_err) - end - assert(src_stat) - if uv.fs_lstat(dest) then - local date_str = vim.fn.strftime(" %Y-%m-%dT%H:%M:%S") - local name_pieces = vim.split(basename, ".", { plain = true }) - if #name_pieces > 1 then - table.insert(name_pieces, #name_pieces - 1, date_str) - basename = table.concat(name_pieces) - else - basename = basename .. date_str - end - dest = fs.join(trash_dir, basename) - end - - local stat_type = src_stat.type - fs.recursive_move(stat_type, path, dest, vim.schedule_wrap(cb)) - end) - ) -end - -return M diff --git a/lua/oil/adapters/trash/windows.lua b/lua/oil/adapters/trash/windows.lua deleted file mode 100644 index f7634e1..0000000 --- a/lua/oil/adapters/trash/windows.lua +++ /dev/null @@ -1,410 +0,0 @@ -local util = require("oil.util") -local uv = vim.uv or vim.loop -local cache = require("oil.cache") -local config = require("oil.config") -local constants = require("oil.constants") -local files = require("oil.adapters.files") -local fs = require("oil.fs") -local powershell_trash = require("oil.adapters.trash.windows.powershell-trash") - -local FIELD_META = constants.FIELD_META -local FIELD_TYPE = constants.FIELD_TYPE - -local M = {} - ----@return string -local function get_trash_dir() - local cwd = assert(vim.fn.getcwd()) - local trash_dir = cwd:sub(1, 3) .. "$Recycle.Bin" - if vim.fn.isdirectory(trash_dir) == 1 then - return trash_dir - end - trash_dir = "C:\\$Recycle.Bin" - if vim.fn.isdirectory(trash_dir) == 1 then - return trash_dir - end - error("No trash found") -end - ----@param path string ----@return string -local win_addslash = function(path) - if not vim.endswith(path, "\\") then - return path .. "\\" - else - return path - end -end - ----@class oil.WindowsTrashInfo ----@field trash_file string ----@field original_path string ----@field deletion_date integer ----@field info_file? string - ----@param url string ----@param column_defs string[] ----@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) -M.list = function(url, column_defs, cb) - local _, path = util.parse_url(url) - path = fs.posix_to_os_path(assert(path)) - - local trash_dir = get_trash_dir() - local show_all_files = fs.is_subpath(path, trash_dir) - - powershell_trash.list_raw_entries(function(err, raw_entries) - if err then - cb(err) - return - end - - local raw_displayed_entries = vim.tbl_filter( - ---@param entry {IsFolder: boolean, DeletionDate: integer, Name: string, Path: string, OriginalPath: string} - function(entry) - local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ":h"))) - local is_in_path = path == parent - local is_subpath = fs.is_subpath(path, parent) - return is_in_path or is_subpath or show_all_files - end, - raw_entries - ) - local displayed_entries = vim.tbl_map( - ---@param entry {IsFolder: boolean, DeletionDate: integer, Name: string, Path: string, OriginalPath: string} - ---@return {[1]:nil, [2]:string, [3]:string, [4]:{stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}} - function(entry) - local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ":h"))) - - --- @type oil.InternalEntry - local cache_entry - if path == parent or show_all_files then - local deleted_file_tail = assert(vim.fn.fnamemodify(entry.Path, ":t")) - local deleted_file_head = assert(vim.fn.fnamemodify(entry.Path, ":h")) - local info_file_head = deleted_file_head - --- @type string? - local info_file - cache_entry = - cache.create_entry(url, deleted_file_tail, entry.IsFolder and "directory" or "file") - -- info_file on windows has the following format: $I<6 char hash>. - -- the hash is the same for the deleted file and the info file - -- so, we take the hash (and extension) from the deleted file - -- - -- see https://superuser.com/questions/368890/how-does-the-recycle-bin-in-windows-work/1736690#1736690 - local info_file_tail = deleted_file_tail:match("^%$R(.*)$") --[[@as string?]] - if info_file_tail then - info_file_tail = "$I" .. info_file_tail - info_file = info_file_head .. "\\" .. info_file_tail - end - cache_entry[FIELD_META] = { - stat = nil, - ---@type oil.WindowsTrashInfo - trash_info = { - trash_file = entry.Path, - original_path = entry.OriginalPath, - deletion_date = entry.DeletionDate, - info_file = info_file, - }, - display_name = entry.Name, - } - end - if path ~= parent and (show_all_files or fs.is_subpath(path, parent)) then - local name = parent:sub(path:len() + 1) - local next_par = vim.fs.dirname(name) - while next_par ~= "." do - name = next_par - next_par = vim.fs.dirname(name) - cache_entry = cache.create_entry(url, name, "directory") - - cache_entry[FIELD_META] = {} - end - end - return cache_entry - end, - raw_displayed_entries - ) - cb(nil, displayed_entries) - end) -end - -M.is_modifiable = function(_bufnr) - return true -end - -local current_year --- Make sure we run this import-time effect in the main loop (mostly for tests) -vim.schedule(function() - current_year = vim.fn.strftime("%Y") -end) - -local file_columns = {} -file_columns.mtime = { - render = function(entry, conf) - local meta = entry[FIELD_META] - if not meta then - return nil - end - ---@type oil.WindowsTrashInfo - local trash_info = meta.trash_info - local time = trash_info and trash_info.deletion_date - if not time then - return nil - end - local fmt = conf and conf.format - local ret - if fmt then - ret = vim.fn.strftime(fmt, time) - else - local year = vim.fn.strftime("%Y", time) - if year ~= current_year then - ret = vim.fn.strftime("%b %d %Y", time) - else - ret = vim.fn.strftime("%b %d %H:%M", time) - end - end - return ret - end, - - get_sort_value = function(entry) - local meta = entry[FIELD_META] - ---@type nil|oil.WindowsTrashInfo - local trash_info = meta and meta.trash_info - if trash_info and trash_info.deletion_date then - return trash_info.deletion_date - else - return 0 - end - end, - - parse = function(line, conf) - local fmt = conf and conf.format - local pattern - if fmt then - pattern = fmt:gsub("%%.", "%%S+") - else - pattern = "%S+%s+%d+%s+%d%d:?%d%d" - end - return line:match("^(" .. pattern .. ")%s+(.+)$") - end, -} - ----@param name string ----@return nil|oil.ColumnDefinition -M.get_column = function(name) - return file_columns[name] -end - ----@param action oil.Action ----@return boolean -M.filter_action = function(action) - if action.type == "create" then - return false - elseif action.type == "delete" then - local entry = assert(cache.get_entry_by_url(action.url)) - local meta = entry[FIELD_META] - return meta ~= nil and meta.trash_info ~= nil - elseif action.type == "move" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - return src_adapter.name == "files" or dest_adapter.name == "files" - elseif action.type == "copy" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - return src_adapter.name == "files" or dest_adapter.name == "files" - else - error(string.format("Bad action type '%s'", action.type)) - end -end - ----@param url string ----@param callback fun(url: string) -M.normalize_url = function(url, callback) - local scheme, path = util.parse_url(url) - assert(path) - local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p") - assert(os_path) - uv.fs_realpath( - os_path, - vim.schedule_wrap(function(_err, new_os_path) - local realpath = new_os_path or os_path - callback(scheme .. util.addslash(fs.os_to_posix_path(realpath))) - end) - ) -end - ----@param url string ----@param entry oil.Entry ----@param cb fun(path: string) -M.get_entry_path = function(url, entry, cb) - local internal_entry = assert(cache.get_entry_by_id(entry.id)) - local meta = internal_entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]] - local trash_info = meta and meta.trash_info - if not trash_info then - -- This is a subpath in the trash - M.normalize_url(url, cb) - return - end - - local path = fs.os_to_posix_path(trash_info.trash_file) - if entry.type == "directory" then - path = win_addslash(path) - end - cb("oil://" .. path) -end - ----@param err oil.ParseError ----@return boolean -M.filter_error = function(err) - if err.message == "Duplicate filename" then - return false - end - return true -end - ----@param action oil.Action ----@return string -M.render_action = function(action) - if action.type == "delete" then - local entry = assert(cache.get_entry_by_url(action.url)) - local meta = entry[FIELD_META] - ---@type oil.WindowsTrashInfo - local trash_info = assert(meta).trash_info - local short_path = fs.shorten_path(trash_info.original_path) - return string.format(" PURGE %s", short_path) - elseif action.type == "move" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - if src_adapter.name == "files" then - local _, path = util.parse_url(action.src_url) - assert(path) - local short_path = files.to_short_os_path(path, action.entry_type) - return string.format(" TRASH %s", short_path) - elseif dest_adapter.name == "files" then - local _, path = util.parse_url(action.dest_url) - assert(path) - local short_path = files.to_short_os_path(path, action.entry_type) - return string.format("RESTORE %s", short_path) - else - error("Must be moving files into or out of trash") - end - elseif action.type == "copy" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - if src_adapter.name == "files" then - local _, path = util.parse_url(action.src_url) - assert(path) - local short_path = files.to_short_os_path(path, action.entry_type) - return string.format(" COPY %s -> TRASH", short_path) - elseif dest_adapter.name == "files" then - local _, path = util.parse_url(action.dest_url) - assert(path) - local short_path = files.to_short_os_path(path, action.entry_type) - return string.format("RESTORE %s", short_path) - else - error("Must be copying files into or out of trash") - end - else - error(string.format("Bad action type '%s'", action.type)) - end -end - ----@param trash_info oil.WindowsTrashInfo ----@param cb fun(err?: string, raw_entries: oil.WindowsRawEntry[]?) -local purge = function(trash_info, cb) - fs.recursive_delete("file", trash_info.info_file, function(err) - if err then - return cb(err) - end - fs.recursive_delete("file", trash_info.trash_file, cb) - end) -end - ----@param path string ----@param type string ----@param cb fun(err?: string, trash_info?: oil.TrashInfo) -local function create_trash_info_and_copy(path, type, cb) - local temp_path = path .. "temp" - -- create a temporary copy on the same location - fs.recursive_copy( - type, - path, - temp_path, - vim.schedule_wrap(function(err) - if err then - return cb(err) - end - -- delete original file - M.delete_to_trash(path, function(err2) - if err2 then - return cb(err2) - end - -- rename temporary copy to the original file name - fs.recursive_move(type, temp_path, path, cb) - end) - end) - ) -end - ----@param action oil.Action ----@param cb fun(err: nil|string) -M.perform_action = function(action, cb) - if action.type == "delete" then - local entry = assert(cache.get_entry_by_url(action.url)) - local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]] - local trash_info = meta and meta.trash_info - - purge(trash_info, cb) - elseif action.type == "move" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - if src_adapter.name == "files" then - local _, path = util.parse_url(action.src_url) - M.delete_to_trash(assert(path), cb) - elseif dest_adapter.name == "files" then - -- Restore - local _, dest_path = util.parse_url(action.dest_url) - assert(dest_path) - dest_path = fs.posix_to_os_path(dest_path) - local entry = assert(cache.get_entry_by_url(action.src_url)) - local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]] - local trash_info = meta and meta.trash_info - fs.recursive_move(action.entry_type, trash_info.trash_file, dest_path, function(err) - if err then - return cb(err) - end - uv.fs_unlink(trash_info.info_file, cb) - end) - end - elseif action.type == "copy" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - if src_adapter.name == "files" then - local _, path = util.parse_url(action.src_url) - assert(path) - path = fs.posix_to_os_path(path) - local entry = assert(cache.get_entry_by_url(action.src_url)) - create_trash_info_and_copy(path, entry[FIELD_TYPE], cb) - elseif dest_adapter.name == "files" then - -- Restore - local _, dest_path = util.parse_url(action.dest_url) - assert(dest_path) - dest_path = fs.posix_to_os_path(dest_path) - local entry = assert(cache.get_entry_by_url(action.src_url)) - local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]] - local trash_info = meta and meta.trash_info - fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb) - else - error("Must be moving files into or out of trash") - end - else - cb(string.format("Bad action type: %s", action.type)) - end -end - -M.supported_cross_adapter_actions = { files = "move" } - ----@param path string ----@param cb fun(err?: string) -M.delete_to_trash = function(path, cb) - powershell_trash.delete_to_trash(path, cb) -end - -return M diff --git a/lua/oil/adapters/trash/windows/powershell-connection.lua b/lua/oil/adapters/trash/windows/powershell-connection.lua deleted file mode 100644 index 332defb..0000000 --- a/lua/oil/adapters/trash/windows/powershell-connection.lua +++ /dev/null @@ -1,123 +0,0 @@ ----@class (exact) oil.PowershellCommand ----@field cmd string ----@field cb fun(err?: string, output?: string) ----@field running? boolean - ----@class oil.PowershellConnection ----@field private jid integer ----@field private execution_error? string ----@field private commands oil.PowershellCommand[] ----@field private stdout string[] ----@field private is_reading_data boolean -local PowershellConnection = {} - ----@param init_command? string ----@return oil.PowershellConnection -function PowershellConnection.new(init_command) - local self = setmetatable({ - commands = {}, - stdout = {}, - is_reading_data = false, - }, { __index = PowershellConnection }) - - self:_init(init_command) - - ---@type oil.PowershellConnection - return self -end - ----@param init_command? string -function PowershellConnection:_init(init_command) - -- For some reason beyond my understanding, at least one of the following - -- things requires `noshellslash` to avoid the embeded powershell process to - -- send only "" to the stdout (never calling the callback because - -- "===DONE(True)===" is never sent to stdout) - -- * vim.fn.jobstart - -- * cmd.exe - -- * powershell.exe - local saved_shellslash = vim.o.shellslash - vim.o.shellslash = false - - -- 65001 is the UTF-8 codepage - -- powershell needs to be launched with the UTF-8 codepage to use it for both stdin and stdout - local jid = vim.fn.jobstart({ - "cmd", - "/c", - '"chcp 65001 && powershell -NoProfile -NoLogo -ExecutionPolicy Bypass -NoExit -Command -"', - }, { - ---@param data string[] - on_stdout = function(_, data) - for _, fragment in ipairs(data) do - if fragment:find("===DONE%((%a+)%)===") then - self.is_reading_data = false - local output = table.concat(self.stdout, "") - local cb = self.commands[1].cb - table.remove(self.commands, 1) - local success = fragment:match("===DONE%((%a+)%)===") - if success == "True" then - cb(nil, output) - elseif success == "False" then - cb(success .. ": " .. output, output) - end - self.stdout = {} - self:_consume() - elseif self.is_reading_data then - table.insert(self.stdout, fragment) - end - end - end, - }) - vim.o.shellslash = saved_shellslash - - if jid == 0 then - self:_set_error("passed invalid arguments to 'powershell'") - elseif jid == -1 then - self:_set_error("'powershell' is not executable") - else - self.jid = jid - end - - if init_command then - table.insert(self.commands, { cmd = init_command, cb = function() end }) - self:_consume() - end -end - ----@param command string ----@param cb fun(err?: string, output?: string[]) -function PowershellConnection:run(command, cb) - if self.execution_error then - cb(self.execution_error) - else - table.insert(self.commands, { cmd = command, cb = cb }) - self:_consume() - end -end - -function PowershellConnection:_consume() - if not vim.tbl_isempty(self.commands) then - local cmd = self.commands[1] - if not cmd.running then - cmd.running = true - self.is_reading_data = true - -- $? contains the execution status of the last command. - -- see https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-7.4#section-1 - vim.api.nvim_chan_send(self.jid, cmd.cmd .. '\nWrite-Host "===DONE($?)==="\n') - end - end -end - ----@param err string -function PowershellConnection:_set_error(err) - if self.execution_error then - return - end - self.execution_error = err - local commands = self.commands - self.commands = {} - for _, cmd in ipairs(commands) do - cmd.cb(err) - end -end - -return PowershellConnection diff --git a/lua/oil/adapters/trash/windows/powershell-trash.lua b/lua/oil/adapters/trash/windows/powershell-trash.lua deleted file mode 100644 index d050bb0..0000000 --- a/lua/oil/adapters/trash/windows/powershell-trash.lua +++ /dev/null @@ -1,78 +0,0 @@ --- A wrapper around trash operations using windows powershell -local Powershell = require("oil.adapters.trash.windows.powershell-connection") - ----@class oil.WindowsRawEntry ----@field IsFolder boolean ----@field DeletionDate integer ----@field Name string ----@field Path string ----@field OriginalPath string - -local M = {} - --- 0xa is the constant for Recycle Bin. See https://learn.microsoft.com/en-us/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants -local list_entries_init = [[ -$shell = New-Object -ComObject 'Shell.Application' -$folder = $shell.NameSpace(0xa) -]] - -local list_entries_cmd = [[ -$data = @(foreach ($i in $folder.items()) - { - @{ - IsFolder=$i.IsFolder; - DeletionDate=([DateTimeOffset]$i.extendedproperty('datedeleted')).ToUnixTimeSeconds(); - Name=$i.Name; - Path=$i.Path; - OriginalPath=-join($i.ExtendedProperty('DeletedFrom'), "\", $i.Name) - } - }) -ConvertTo-Json $data -Compress -]] - ----@type nil|oil.PowershellConnection -local list_entries_powershell - ----@param cb fun(err?: string, raw_entries?: oil.WindowsRawEntry[]) -M.list_raw_entries = function(cb) - if not list_entries_powershell then - list_entries_powershell = Powershell.new(list_entries_init) - end - list_entries_powershell:run(list_entries_cmd, function(err, string) - if err then - cb(err) - return - end - - local ok, value = pcall(vim.json.decode, string) - if not ok then - cb(value) - return - end - cb(nil, value) - end) -end - --- 0 is the constant for Windows Desktop. See https://learn.microsoft.com/en-us/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants -local delete_init = [[ -$shell = New-Object -ComObject 'Shell.Application' -$folder = $shell.NameSpace(0) -]] -local delete_cmd = [[ -$path = Get-Item '%s' -$folder.ParseName($path.FullName).InvokeVerb('delete') -]] - ----@type nil|oil.PowershellConnection -local delete_to_trash_powershell - ----@param path string ----@param cb fun(err?: string) -M.delete_to_trash = function(path, cb) - if not delete_to_trash_powershell then - delete_to_trash_powershell = Powershell.new(delete_init) - end - delete_to_trash_powershell:run((delete_cmd):format(path:gsub("'", "''")), cb) -end - -return M diff --git a/lua/oil/cache.lua b/lua/oil/cache.lua index ef5597c..da04b49 100644 --- a/lua/oil/cache.lua +++ b/lua/oil/cache.lua @@ -4,12 +4,10 @@ local M = {} local FIELD_ID = constants.FIELD_ID local FIELD_NAME = constants.FIELD_NAME -local FIELD_META = constants.FIELD_META local next_id = 1 -- Map> ----@type table> local url_directory = {} ---@type table @@ -54,7 +52,10 @@ M.create_entry = function(parent_url, name, type) if entry then return entry end - return { nil, name, type } + local id = next_id + next_id = next_id + 1 + _cached_id_fmt = nil + return { id, name, type } end ---@param parent_url string @@ -67,12 +68,6 @@ M.store_entry = function(parent_url, entry) url_directory[parent_url] = parent end local id = entry[FIELD_ID] - if id == nil then - id = next_id - next_id = next_id + 1 - entry[FIELD_ID] = id - _cached_id_fmt = nil - end local name = entry[FIELD_NAME] parent[name] = entry local tmp_dir = tmp_url_directory[parent_url] @@ -93,14 +88,12 @@ M.create_and_store_entry = function(parent_url, name, type) return entry end ----@param parent_url string M.begin_update_url = function(parent_url) parent_url = util.addslash(parent_url) tmp_url_directory[parent_url] = url_directory[parent_url] url_directory[parent_url] = {} end ----@param parent_url string M.end_update_url = function(parent_url) parent_url = util.addslash(parent_url) if not tmp_url_directory[parent_url] then @@ -120,16 +113,6 @@ M.get_entry_by_id = function(id) return entries_by_id[id] end ----@param url string ----@return nil|oil.InternalEntry -M.get_entry_by_url = function(url) - local scheme, path = util.parse_url(url) - assert(path) - local parent_url = scheme .. vim.fn.fnamemodify(path, ":h") - local basename = vim.fn.fnamemodify(path, ":t") - return M.list_url(parent_url)[basename] -end - ---@param id integer ---@return string M.get_parent_url = function(id) @@ -141,23 +124,27 @@ M.get_parent_url = function(id) end ---@param url string ----@return table +---@return oil.InternalEntry[] M.list_url = function(url) url = util.addslash(url) return url_directory[url] or {} end ----@param action oil.Action +M.get_entry_by_url = function(url) + local parent, name = url:match("^(.+)/([^/]+)$") + local cache = url_directory[parent] + return cache and cache[name] +end + +---@param oil.Action M.perform_action = function(action) if action.type == "create" then local scheme, path = util.parse_url(action.url) - assert(path) local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h")) local name = vim.fn.fnamemodify(path, ":t") M.create_and_store_entry(parent_url, name, action.entry_type) elseif action.type == "delete" then local scheme, path = util.parse_url(action.url) - assert(path) local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h")) local name = vim.fn.fnamemodify(path, ":t") local entry = url_directory[parent_url][name] @@ -166,13 +153,11 @@ M.perform_action = function(action) parent_url_by_id[entry[FIELD_ID]] = nil elseif action.type == "move" then local src_scheme, src_path = util.parse_url(action.src_url) - assert(src_path) local src_parent_url = util.addslash(src_scheme .. vim.fn.fnamemodify(src_path, ":h")) local src_name = vim.fn.fnamemodify(src_path, ":t") local entry = url_directory[src_parent_url][src_name] local dest_scheme, dest_path = util.parse_url(action.dest_url) - assert(dest_path) local dest_parent_url = util.addslash(dest_scheme .. vim.fn.fnamemodify(dest_path, ":h")) local dest_name = vim.fn.fnamemodify(dest_path, ":t") @@ -182,22 +167,18 @@ M.perform_action = function(action) dest_parent = {} url_directory[dest_parent_url] = dest_parent end - -- We have to clear the metadata because it can be inaccurate after the move - entry[FIELD_META] = nil dest_parent[dest_name] = entry parent_url_by_id[entry[FIELD_ID]] = dest_parent_url entry[FIELD_NAME] = dest_name util.update_moved_buffers(action.entry_type, action.src_url, action.dest_url) elseif action.type == "copy" then local scheme, path = util.parse_url(action.dest_url) - assert(path) local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h")) local name = vim.fn.fnamemodify(path, ":t") M.create_and_store_entry(parent_url, name, action.entry_type) elseif action.type == "change" then -- Cache doesn't need to update else - ---@diagnostic disable-next-line: undefined-field error(string.format("Bad action type: '%s'", action.type)) end end diff --git a/lua/oil/clipboard.lua b/lua/oil/clipboard.lua deleted file mode 100644 index 04498f5..0000000 --- a/lua/oil/clipboard.lua +++ /dev/null @@ -1,370 +0,0 @@ -local cache = require("oil.cache") -local columns = require("oil.columns") -local config = require("oil.config") -local fs = require("oil.fs") -local oil = require("oil") -local parser = require("oil.mutator.parser") -local util = require("oil.util") -local view = require("oil.view") - -local M = {} - ----@return "wayland"|"x11"|nil -local function get_linux_session_type() - local xdg_session_type = vim.env.XDG_SESSION_TYPE - if not xdg_session_type then - return - end - xdg_session_type = xdg_session_type:lower() - if xdg_session_type:find("x11") then - return "x11" - elseif xdg_session_type:find("wayland") then - return "wayland" - else - return nil - end -end - ----@return boolean -local function is_linux_desktop_gnome() - local cur_desktop = vim.env.XDG_CURRENT_DESKTOP - local session_desktop = vim.env.XDG_SESSION_DESKTOP - local idx = session_desktop and session_desktop:lower():find("gnome") - or cur_desktop and cur_desktop:lower():find("gnome") - return idx ~= nil or cur_desktop == "X-Cinnamon" or cur_desktop == "XFCE" -end - ----@param winid integer ----@param entry oil.InternalEntry ----@param column_defs oil.ColumnSpec[] ----@param adapter oil.Adapter ----@param bufnr integer -local function write_pasted(winid, entry, column_defs, adapter, bufnr) - local col_width = {} - for i in ipairs(column_defs) do - col_width[i + 1] = 1 - end - local line_table = - { view.format_entry_cols(entry, column_defs, col_width, adapter, false, bufnr) } - local lines, _ = util.render_table(line_table, col_width) - local pos = vim.api.nvim_win_get_cursor(winid) - vim.api.nvim_buf_set_lines(bufnr, pos[1], pos[1], true, lines) -end - ----@param parent_url string ----@param entry oil.InternalEntry -local function remove_entry_from_parent_buffer(parent_url, entry) - local bufnr = vim.fn.bufadd(parent_url) - assert(vim.api.nvim_buf_is_loaded(bufnr), "Expected parent buffer to be loaded during paste") - local adapter = assert(util.get_adapter(bufnr)) - local column_defs = columns.get_supported_columns(adapter) - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - for i, line in ipairs(lines) do - local result = parser.parse_line(adapter, line, column_defs) - if result and result.entry == entry then - vim.api.nvim_buf_set_lines(bufnr, i - 1, i, false, {}) - return - end - end - local exported = util.export_entry(entry) - vim.notify( - string.format("Error: could not delete original file '%s'", exported.name), - vim.log.levels.ERROR - ) -end - ----@param paths string[] ----@param delete_original? boolean -local function paste_paths(paths, delete_original) - local bufnr = vim.api.nvim_get_current_buf() - local scheme = "oil://" - local adapter = assert(config.get_adapter_by_scheme(scheme)) - local column_defs = columns.get_supported_columns(scheme) - local winid = vim.api.nvim_get_current_win() - - local parent_urls = {} - local pending_paths = {} - - -- Handle as many paths synchronously as possible - for _, path in ipairs(paths) do - -- Trim the trailing slash off directories - if vim.endswith(path, "/") then - path = path:sub(1, -2) - end - - local ori_entry = cache.get_entry_by_url(scheme .. path) - local parent_url = util.addslash(scheme .. vim.fs.dirname(path)) - if ori_entry then - write_pasted(winid, ori_entry, column_defs, adapter, bufnr) - if delete_original then - remove_entry_from_parent_buffer(parent_url, ori_entry) - end - else - parent_urls[parent_url] = true - table.insert(pending_paths, path) - end - end - - -- If all paths could be handled synchronously, we're done - if #pending_paths == 0 then - return - end - - -- Process the remaining paths by asynchronously loading them - local cursor = vim.api.nvim_win_get_cursor(winid) - local complete_loading = util.cb_collect(#vim.tbl_keys(parent_urls), function(err) - if err then - vim.notify(string.format("Error loading parent directory: %s", err), vim.log.levels.ERROR) - else - -- Something in this process moves the cursor to the top of the window, so have to restore it - vim.api.nvim_win_set_cursor(winid, cursor) - - for _, path in ipairs(pending_paths) do - local ori_entry = cache.get_entry_by_url(scheme .. path) - if ori_entry then - write_pasted(winid, ori_entry, column_defs, adapter, bufnr) - if delete_original then - local parent_url = util.addslash(scheme .. vim.fs.dirname(path)) - remove_entry_from_parent_buffer(parent_url, ori_entry) - end - else - vim.notify( - string.format("The pasted file '%s' could not be found", path), - vim.log.levels.ERROR - ) - end - end - end - end) - - for parent_url, _ in pairs(parent_urls) do - local new_bufnr = vim.api.nvim_create_buf(false, false) - vim.api.nvim_buf_set_name(new_bufnr, parent_url) - oil.load_oil_buffer(new_bufnr) - util.run_after_load(new_bufnr, complete_loading) - end -end - ----@return integer start ----@return integer end -local function range_from_selection() - -- [bufnum, lnum, col, off]; both row and column 1-indexed - local start = vim.fn.getpos("v") - local end_ = vim.fn.getpos(".") - local start_row = start[2] - local end_row = end_[2] - - if start_row > end_row then - start_row, end_row = end_row, start_row - end - - return start_row, end_row -end - -M.copy_to_system_clipboard = function() - local dir = oil.get_current_dir() - if not dir then - vim.notify("System clipboard only works for local files", vim.log.levels.ERROR) - return - end - - local entries = {} - local mode = vim.api.nvim_get_mode().mode - if mode == "v" or mode == "V" then - if fs.is_mac then - vim.notify( - "Copying multiple paths to clipboard is not supported on mac", - vim.log.levels.ERROR - ) - return - end - local start_row, end_row = range_from_selection() - for i = start_row, end_row do - table.insert(entries, oil.get_entry_on_line(0, i)) - end - - -- leave visual mode - vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "n", true) - else - table.insert(entries, oil.get_cursor_entry()) - end - - -- This removes holes in the list-like table - entries = vim.tbl_values(entries) - - if #entries == 0 then - vim.notify("Could not find local file under cursor", vim.log.levels.WARN) - return - end - local paths = {} - for _, entry in ipairs(entries) do - table.insert(paths, dir .. entry.name) - end - local cmd = {} - local stdin - if fs.is_mac then - cmd = { - "osascript", - "-e", - "on run args", - "-e", - "set the clipboard to POSIX file (first item of args)", - "-e", - "end run", - paths[1], - } - elseif fs.is_linux then - local xdg_session_type = get_linux_session_type() - if xdg_session_type == "x11" then - vim.list_extend(cmd, { "xclip", "-i", "-selection", "clipboard" }) - elseif xdg_session_type == "wayland" then - table.insert(cmd, "wl-copy") - else - vim.notify("System clipboard not supported, check $XDG_SESSION_TYPE", vim.log.levels.ERROR) - return - end - local urls = {} - for _, path in ipairs(paths) do - table.insert(urls, "file://" .. path) - end - if is_linux_desktop_gnome() then - stdin = string.format("copy\n%s\0", table.concat(urls, "\n")) - vim.list_extend(cmd, { "-t", "x-special/gnome-copied-files" }) - else - stdin = table.concat(urls, "\n") .. "\n" - vim.list_extend(cmd, { "-t", "text/uri-list" }) - end - else - vim.notify("System clipboard not supported on Windows", vim.log.levels.ERROR) - return - end - - if vim.fn.executable(cmd[1]) == 0 then - vim.notify(string.format("Could not find executable '%s'", cmd[1]), vim.log.levels.ERROR) - return - end - local stderr = "" - local jid = vim.fn.jobstart(cmd, { - stderr_buffered = true, - on_stderr = function(_, data) - stderr = table.concat(data, "\n") - end, - on_exit = function(j, exit_code) - if exit_code ~= 0 then - vim.notify( - string.format("Error copying '%s' to system clipboard\n%s", vim.inspect(paths), stderr), - vim.log.levels.ERROR - ) - else - if #paths == 1 then - vim.notify(string.format("Copied '%s' to system clipboard", paths[1])) - else - vim.notify(string.format("Copied %d files to system clipboard", #paths)) - end - end - end, - }) - assert(jid > 0, "Failed to start job") - if stdin then - vim.api.nvim_chan_send(jid, stdin) - vim.fn.chanclose(jid, "stdin") - end -end - ----@param lines string[] ----@return string[] -local function handle_paste_output_mac(lines) - local ret = {} - for _, line in ipairs(lines) do - if not line:match("^%s*$") then - table.insert(ret, line) - end - end - return ret -end - ----@param lines string[] ----@return string[] -local function handle_paste_output_linux(lines) - local ret = {} - for _, line in ipairs(lines) do - local path = line:match("^file://(.+)$") - if path then - table.insert(ret, util.url_unescape(path)) - end - end - return ret -end - ----@param delete_original? boolean Delete the source file after pasting -M.paste_from_system_clipboard = function(delete_original) - local dir = oil.get_current_dir() - if not dir then - return - end - local cmd = {} - local handle_paste_output - if fs.is_mac then - cmd = { - "osascript", - "-e", - "on run", - "-e", - "POSIX path of (the clipboard as «class furl»)", - "-e", - "end run", - } - handle_paste_output = handle_paste_output_mac - elseif fs.is_linux then - local xdg_session_type = get_linux_session_type() - if xdg_session_type == "x11" then - vim.list_extend(cmd, { "xclip", "-o", "-selection", "clipboard" }) - elseif xdg_session_type == "wayland" then - table.insert(cmd, "wl-paste") - else - vim.notify("System clipboard not supported, check $XDG_SESSION_TYPE", vim.log.levels.ERROR) - return - end - if is_linux_desktop_gnome() then - vim.list_extend(cmd, { "-t", "x-special/gnome-copied-files" }) - else - vim.list_extend(cmd, { "-t", "text/uri-list" }) - end - handle_paste_output = handle_paste_output_linux - else - vim.notify("System clipboard not supported on Windows", vim.log.levels.ERROR) - return - end - local paths - local stderr = "" - if vim.fn.executable(cmd[1]) == 0 then - vim.notify(string.format("Could not find executable '%s'", cmd[1]), vim.log.levels.ERROR) - return - end - local jid = vim.fn.jobstart(cmd, { - stdout_buffered = true, - stderr_buffered = true, - on_stdout = function(j, data) - local lines = vim.split(table.concat(data, "\n"), "\r?\n") - paths = handle_paste_output(lines) - end, - on_stderr = function(_, data) - stderr = table.concat(data, "\n") - end, - on_exit = function(j, exit_code) - if exit_code ~= 0 or not paths then - vim.notify( - string.format("Error pasting from system clipboard: %s", stderr), - vim.log.levels.ERROR - ) - elseif #paths == 0 then - vim.notify("No valid files found in system clipboard", vim.log.levels.WARN) - else - paste_paths(paths, delete_original) - end - end, - }) - assert(jid > 0, "Failed to start job") -end - -return M diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua index 975576f..43a4d14 100644 --- a/lua/oil/columns.lua +++ b/lua/oil/columns.lua @@ -1,6 +1,7 @@ local config = require("oil.config") local constants = require("oil.constants") local util = require("oil.util") +local has_devicons, devicons = pcall(require, "nvim-web-devicons") local M = {} local FIELD_NAME = constants.FIELD_NAME @@ -9,16 +10,12 @@ local FIELD_META = constants.FIELD_META local all_columns = {} ----@alias oil.ColumnSpec string|{[1]: string, [string]: any} +---@alias oil.ColumnSpec string|table ----@class (exact) oil.ColumnDefinition ----@field render fun(entry: oil.InternalEntry, conf: nil|table, bufnr: integer): nil|oil.TextChunk +---@class oil.ColumnDefinition +---@field render fun(entry: oil.InternalEntry, conf: nil|table): nil|oil.TextChunk ---@field parse fun(line: string, conf: nil|table): nil|string, nil|string ----@field compare? fun(entry: oil.InternalEntry, parsed_value: any): boolean ----@field render_action? fun(action: oil.ChangeAction): string ----@field perform_action? fun(action: oil.ChangeAction, callback: fun(err: nil|string)) ----@field get_sort_value? fun(entry: oil.InternalEntry): number|string ----@field create_sort_value_factory? fun(num_entries: integer): fun(entry: oil.InternalEntry): number|string +---@field meta_fields nil|table ---@param name string ---@param column oil.ColumnDefinition @@ -29,7 +26,7 @@ end ---@param adapter oil.Adapter ---@param defn oil.ColumnSpec ---@return nil|oil.ColumnDefinition -M.get_column = function(adapter, defn) +local function get_column(adapter, defn) local name = util.split_config(defn) return all_columns[name] or adapter.get_column(name) end @@ -43,34 +40,82 @@ M.get_supported_columns = function(adapter_or_scheme) else adapter = adapter_or_scheme end - assert(adapter) local ret = {} for _, def in ipairs(config.columns) do - if M.get_column(adapter, def) then + if get_column(adapter, def) then table.insert(ret, def) end end return ret end -local EMPTY = { "-", "OilEmpty" } +---@param adapter oil.Adapter +---@param column_defs table[] +---@return fun(parent_url: string, entry: oil.InternalEntry, cb: fun(err: nil|string)) +M.get_metadata_fetcher = function(adapter, column_defs) + local keyfetches = {} + local num_keys = 0 + for _, def in ipairs(column_defs) do + local name = util.split_config(def) + local column = get_column(adapter, name) + if column and column.meta_fields then + for k, v in pairs(column.meta_fields) do + if not keyfetches[k] then + keyfetches[k] = v + num_keys = num_keys + 1 + end + end + end + end + if num_keys == 0 then + return function(_, _, cb) + cb() + end + end + return function(parent_url, entry, cb) + cb = util.cb_collect(num_keys, cb) + local meta = {} + entry[FIELD_META] = meta + for k, v in pairs(keyfetches) do + v(parent_url, entry, function(err, value) + if err then + cb(err) + else + meta[k] = value + cb() + end + end) + end + end +end -M.EMPTY = EMPTY +local EMPTY = { "-", "Comment" } ---@param adapter oil.Adapter ---@param col_def oil.ColumnSpec ---@param entry oil.InternalEntry ----@param bufnr integer ---@return oil.TextChunk -M.render_col = function(adapter, col_def, entry, bufnr) +M.render_col = function(adapter, col_def, entry) local name, conf = util.split_config(col_def) - local column = M.get_column(adapter, name) + local column = get_column(adapter, name) if not column then -- This shouldn't be possible because supports_col should return false return EMPTY end - local chunk = column.render(entry, conf, bufnr) + -- Make sure all the required metadata exists before attempting to render + if column.meta_fields then + local meta = entry[FIELD_META] + if not meta then + return EMPTY + end + for k in pairs(column.meta_fields) do + if not meta[k] then + return EMPTY + end + end + end + local chunk = column.render(entry, conf) if type(chunk) == "table" then if chunk[1]:match("^%s*$") then return EMPTY @@ -98,13 +143,12 @@ end M.parse_col = function(adapter, line, col_def) local name, conf = util.split_config(col_def) -- If rendering failed, there will just be a "-" - local empty_col, rem = line:match("^%s*(-%s+)(.*)$") - if empty_col then - return nil, rem + if vim.startswith(line, "- ") then + return nil, line:sub(3) end - local column = M.get_column(adapter, name) + local column = get_column(adapter, name) if column then - return column.parse(line:gsub("^%s+", ""), conf) + return column.parse(line, conf) end end @@ -114,7 +158,7 @@ end ---@param parsed_value any ---@return boolean M.compare = function(adapter, col_name, entry, parsed_value) - local column = M.get_column(adapter, col_name) + local column = get_column(adapter, col_name) if column and column.compare then return column.compare(entry, parsed_value) else @@ -126,7 +170,7 @@ end ---@param action oil.ChangeAction ---@return string M.render_change_action = function(adapter, action) - local column = M.get_column(adapter, action.column) + local column = get_column(adapter, action.column) if not column then error(string.format("Received change action for nonexistant column %s", action.column)) end @@ -141,7 +185,7 @@ end ---@param action oil.ChangeAction ---@param callback fun(err: nil|string) M.perform_change_action = function(adapter, action, callback) - local column = M.get_column(adapter, action.column) + local column = get_column(adapter, action.column) if not column then return callback( string.format("Received change action for nonexistant column %s", action.column) @@ -150,35 +194,31 @@ M.perform_change_action = function(adapter, action, callback) column.perform_action(action, callback) end -local icon_provider = util.get_icon_provider() -if icon_provider then +if has_devicons then M.register("icon", { render = function(entry, conf) - local field_type = entry[FIELD_TYPE] + local type = entry[FIELD_TYPE] local name = entry[FIELD_NAME] local meta = entry[FIELD_META] - if field_type == "link" and meta then + if type == "link" and meta then if meta.link then name = meta.link end if meta.link_stat then - field_type = meta.link_stat.type + type = meta.link_stat.type end end - if meta and meta.display_name then - name = meta.display_name + local icon, hl + if type == "directory" then + icon = conf and conf.directory or "" + hl = "OilDirIcon" + else + icon, hl = devicons.get_icon(name) + icon = icon or (conf and conf.default_file or "") end - local icon, hl = icon_provider(field_type, name, conf) if not conf or conf.add_padding ~= false then icon = icon .. " " end - if conf and conf.highlight then - if type(conf.highlight) == "function" then - hl = conf.highlight(icon) - else - hl = conf.highlight - end - end return { icon, hl } end, @@ -192,19 +232,6 @@ local default_type_icons = { directory = "dir", socket = "sock", } ----@param entry oil.InternalEntry ----@return boolean -local function is_entry_directory(entry) - local type = entry[FIELD_TYPE] - if type == "directory" then - return true - elseif type == "link" then - local meta = entry[FIELD_META] - return (meta and meta.link_stat and meta.link_stat.type == "directory") == true - else - return false - end -end M.register("type", { render = function(entry, conf) local entry_type = entry[FIELD_TYPE] @@ -218,57 +245,6 @@ M.register("type", { parse = function(line, conf) return line:match("^(%S+)%s+(.*)$") end, - - get_sort_value = function(entry) - if is_entry_directory(entry) then - return 1 - else - return 2 - end - end, -}) - -local function adjust_number(int) - return string.format("%03d%s", #int, int) -end - -M.register("name", { - render = function(entry, conf) - error("Do not use the name column. It is for sorting only") - end, - - parse = function(line, conf) - error("Do not use the name column. It is for sorting only") - end, - - create_sort_value_factory = function(num_entries) - if - config.view_options.natural_order == false - or (config.view_options.natural_order == "fast" and num_entries > 5000) - then - if config.view_options.case_insensitive then - return function(entry) - return entry[FIELD_NAME]:lower() - end - else - return function(entry) - return entry[FIELD_NAME] - end - end - else - local memo = {} - return function(entry) - if memo[entry] == nil then - local name = entry[FIELD_NAME]:gsub("0*(%d+)", adjust_number) - if config.view_options.case_insensitive then - name = name:lower() - end - memo[entry] = name - end - return memo[entry] - end - end - end, }) return M diff --git a/lua/oil/config.lua b/lua/oil/config.lua index cafa783..305f377 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -1,7 +1,4 @@ local default_config = { - -- Oil will take over directory buffers (e.g. `vim .` or `:e src/`) - -- Set to false if you want some other plugin (e.g. netrw) to open when you edit directories. - default_file_explorer = true, -- Id is automatically added at the beginning, and name at the end -- See :help oil-columns columns = { @@ -24,56 +21,40 @@ local default_config = { spell = false, list = false, conceallevel = 3, - concealcursor = "nvic", + concealcursor = "n", }, - -- Send deleted files to the trash instead of permanently deleting them (:help oil-trash) - delete_to_trash = false, - -- Skip the confirmation popup for simple operations (:help oil.skip_confirm_for_simple_edits) + -- Oil will take over directory buffers (e.g. `vim .` or `:e src/` + default_file_explorer = true, + -- Restore window options to previous values when leaving an oil buffer + restore_win_options = true, + -- Skip the confirmation popup for simple operations skip_confirm_for_simple_edits = false, + -- Deleted files will be removed with the trash_command (below). + delete_to_trash = false, + -- Change this to customize the command used when deleting to trash + trash_command = "trash-put", -- Selecting a new/moved/renamed file or directory will prompt you to save changes first - -- (:help prompt_save_on_select_new_entry) prompt_save_on_select_new_entry = true, - -- Oil will automatically delete hidden buffers after this delay - -- You can set the delay to false to disable cleanup entirely - -- Note that the cleanup process only starts when none of the oil buffers are currently displayed - cleanup_delay_ms = 2000, - lsp_file_methods = { - -- Enable or disable LSP file operations - enabled = true, - -- Time to wait for LSP file operations to complete before skipping - timeout_ms = 1000, - -- Set to true to autosave buffers that are updated with LSP willRenameFiles - -- Set to "unmodified" to only save unmodified buffers - autosave_changes = false, - }, - -- Constrain the cursor to the editable parts of the oil buffer - -- Set to `false` to disable, or "name" to keep it on the file names - constrain_cursor = "editable", - -- Set to true to watch the filesystem for changes and reload oil - watch_for_changes = false, -- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap - -- options with a `callback` (e.g. { callback = function() ... end, desc = "", mode = "n" }) + -- options with a `callback` (e.g. { callback = function() ... end, desc = "", nowait = true }) -- Additionally, if it is a string that matches "actions.", -- it will use the mapping at require("oil.actions"). -- Set to `false` to remove a keymap -- See :help oil-actions for a list of all available actions keymaps = { - ["g?"] = { "actions.show_help", mode = "n" }, + ["g?"] = "actions.show_help", [""] = "actions.select", - [""] = { "actions.select", opts = { vertical = true } }, - [""] = { "actions.select", opts = { horizontal = true } }, - [""] = { "actions.select", opts = { tab = true } }, + [""] = "actions.select_vsplit", + [""] = "actions.select_split", + [""] = "actions.select_tab", [""] = "actions.preview", - [""] = { "actions.close", mode = "n" }, + [""] = "actions.close", [""] = "actions.refresh", - ["-"] = { "actions.parent", mode = "n" }, - ["_"] = { "actions.open_cwd", mode = "n" }, - ["`"] = { "actions.cd", mode = "n" }, - ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" }, - ["gs"] = { "actions.change_sort", mode = "n" }, - ["gx"] = "actions.open_external", - ["g."] = { "actions.toggle_hidden", mode = "n" }, - ["g\\"] = { "actions.toggle_trash", mode = "n" }, + ["-"] = "actions.parent", + ["_"] = "actions.open_cwd", + ["`"] = "actions.cd", + ["~"] = "actions.tcd", + ["g."] = "actions.toggle_hidden", }, -- Set to false to disable all of the above keymaps use_default_keymaps = true, @@ -82,82 +63,31 @@ local default_config = { show_hidden = false, -- This function defines what is considered a "hidden" file is_hidden_file = function(name, bufnr) - local m = name:match("^%.") - return m ~= nil + return vim.startswith(name, ".") end, -- This function defines what will never be shown, even when `show_hidden` is set is_always_hidden = function(name, bufnr) return false end, - -- Sort file names with numbers in a more intuitive order for humans. - -- Can be "fast", true, or false. "fast" will turn it off for large directories. - natural_order = "fast", - -- Sort file and directory names case insensitive - case_insensitive = false, - sort = { - -- sort order can be "asc" or "desc" - -- see :help oil-columns to see which columns are sortable - { "type", "asc" }, - { "name", "asc" }, - }, - -- Customize the highlight group for the file name - highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan) - return nil - end, - }, - -- Extra arguments to pass to SCP when moving/copying files over SSH - extra_scp_args = {}, - -- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3 - extra_s3_args = {}, - -- EXPERIMENTAL support for performing file operations with git - git = { - -- Return true to automatically git add/mv/rm files - add = function(path) - return false - end, - mv = function(src_path, dest_path) - return false - end, - rm = function(path) - return false - end, }, -- Configuration for the floating window in oil.open_float float = { -- Padding around the floating window padding = 2, - -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) max_width = 0, max_height = 0, - border = nil, + border = "rounded", win_options = { - winblend = 0, + winblend = 10, }, - -- optionally override the oil buffers window title with custom function: fun(winid: integer): string - get_win_title = nil, - -- preview_split: Split direction: "auto", "left", "right", "above", "below". - preview_split = "auto", -- This is the config that will be passed to nvim_open_win. -- Change values here to customize the layout override = function(conf) return conf end, }, - -- Configuration for the file preview window - preview_win = { - -- Whether the preview window is automatically updated when the cursor is moved - update_on_cursor_moved = true, - -- How to open the preview window "load"|"scratch"|"fast_scratch" - preview_method = "fast_scratch", - -- A function that returns true to disable preview on a file e.g. to avoid lag - disable_preview = function(filename) - return false - end, - -- Window-local options to use for preview window buffers - win_options = {}, - }, - -- Configuration for the floating action confirmation window - confirmation = { + -- Configuration for the actions floating preview window + preview = { -- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- min_width and max_width can be a single value or a list of mixed integer/float types. -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total" @@ -174,7 +104,7 @@ local default_config = { min_height = { 5, 0.1 }, -- optionally define an integer/float for the exact height of the preview window height = nil, - border = nil, + border = "rounded", win_options = { winblend = 0, }, @@ -187,266 +117,45 @@ local default_config = { max_height = { 10, 0.9 }, min_height = { 5, 0.1 }, height = nil, - border = nil, + border = "rounded", minimized_border = "none", win_options = { winblend = 0, }, }, - -- Configuration for the floating SSH window - ssh = { - border = nil, - }, - -- Configuration for the floating keymaps help window - keymaps_help = { - border = nil, - }, } -- The adapter API hasn't really stabilized yet. We're not ready to advertise or encourage people to -- write their own adapters, and so there's no real reason to edit these config options. For that -- reason, I'm taking them out of the section above so they won't show up in the autogen docs. - --- not "oil-s3://" on older neovim versions, since it doesn't open buffers correctly with a number --- in the name -local oil_s3_string = vim.fn.has("nvim-0.12") == 1 and "oil-s3://" or "oil-sss://" default_config.adapters = { ["oil://"] = "files", ["oil-ssh://"] = "ssh", - [oil_s3_string] = "s3", - ["oil-trash://"] = "trash", } default_config.adapter_aliases = {} --- We want the function in the default config for documentation generation, but if we nil it out --- here we can get some performance wins -default_config.view_options.highlight_filename = nil ----@class oil.Config ----@field adapters table Hidden from SetupOpts ----@field adapter_aliases table Hidden from SetupOpts ----@field silence_scp_warning? boolean Undocumented option ----@field default_file_explorer boolean ----@field columns oil.ColumnSpec[] ----@field buf_options table ----@field win_options table ----@field delete_to_trash boolean ----@field skip_confirm_for_simple_edits boolean ----@field prompt_save_on_select_new_entry boolean ----@field cleanup_delay_ms integer ----@field lsp_file_methods oil.LspFileMethods ----@field constrain_cursor false|"name"|"editable" ----@field watch_for_changes boolean ----@field keymaps table ----@field use_default_keymaps boolean ----@field view_options oil.ViewOptions ----@field extra_scp_args string[] ----@field extra_s3_args string[] ----@field git oil.GitOptions ----@field float oil.FloatWindowConfig ----@field preview_win oil.PreviewWindowConfig ----@field confirmation oil.ConfirmationWindowConfig ----@field progress oil.ProgressWindowConfig ----@field ssh oil.SimpleWindowConfig ----@field keymaps_help oil.SimpleWindowConfig local M = {} --- For backwards compatibility ----@alias oil.setupOpts oil.SetupOpts - ----@class (exact) oil.SetupOpts ----@field default_file_explorer? boolean Oil will take over directory buffers (e.g. `vim .` or `:e src/`). Set to false if you still want to use netrw. ----@field columns? oil.ColumnSpec[] The columns to display. See :help oil-columns. ----@field buf_options? table Buffer-local options to use for oil buffers ----@field win_options? table Window-local options to use for oil buffers ----@field delete_to_trash? boolean Send deleted files to the trash instead of permanently deleting them (:help oil-trash). ----@field skip_confirm_for_simple_edits? boolean Skip the confirmation popup for simple operations (:help oil.skip_confirm_for_simple_edits). ----@field prompt_save_on_select_new_entry? boolean Selecting a new/moved/renamed file or directory will prompt you to save changes first (:help prompt_save_on_select_new_entry). ----@field cleanup_delay_ms? integer Oil will automatically delete hidden buffers after this delay. You can set the delay to false to disable cleanup entirely. Note that the cleanup process only starts when none of the oil buffers are currently displayed. ----@field lsp_file_methods? oil.SetupLspFileMethods Configure LSP file operation integration. ----@field constrain_cursor? false|"name"|"editable" Constrain the cursor to the editable parts of the oil buffer. Set to `false` to disable, or "name" to keep it on the file names. ----@field watch_for_changes? boolean Set to true to watch the filesystem for changes and reload oil. ----@field keymaps? table ----@field use_default_keymaps? boolean Set to false to disable all of the above keymaps ----@field view_options? oil.SetupViewOptions Configure which files are shown and how they are shown. ----@field extra_scp_args? string[] Extra arguments to pass to SCP when moving/copying files over SSH ----@field extra_s3_args? string[] Extra arguments to pass to aws s3 when moving/copying files using aws s3 ----@field git? oil.SetupGitOptions EXPERIMENTAL support for performing file operations with git ----@field float? oil.SetupFloatWindowConfig Configuration for the floating window in oil.open_float ----@field preview_win? oil.SetupPreviewWindowConfig Configuration for the file preview window ----@field confirmation? oil.SetupConfirmationWindowConfig Configuration for the floating action confirmation window ----@field progress? oil.SetupProgressWindowConfig Configuration for the floating progress window ----@field ssh? oil.SetupSimpleWindowConfig Configuration for the floating SSH window ----@field keymaps_help? oil.SetupSimpleWindowConfig Configuration for the floating keymaps help window - ----@class (exact) oil.LspFileMethods ----@field enabled boolean ----@field timeout_ms integer ----@field autosave_changes boolean|"unmodified" Set to true to autosave buffers that are updated with LSP willRenameFiles. Set to "unmodified" to only save unmodified buffers. - ----@class (exact) oil.SetupLspFileMethods ----@field enabled? boolean Enable or disable LSP file operations ----@field timeout_ms? integer Time to wait for LSP file operations to complete before skipping. ----@field autosave_changes? boolean|"unmodified" Set to true to autosave buffers that are updated with LSP willRenameFiles. Set to "unmodified" to only save unmodified buffers. - ----@class (exact) oil.ViewOptions ----@field show_hidden boolean ----@field is_hidden_file fun(name: string, bufnr: integer): boolean ----@field is_always_hidden fun(name: string, bufnr: integer): boolean ----@field natural_order boolean|"fast" ----@field case_insensitive boolean ----@field sort oil.SortSpec[] ----@field highlight_filename? fun(entry: oil.Entry, is_hidden: boolean, is_link_target: boolean, is_link_orphan: boolean, bufnr: integer): string|nil - ----@class (exact) oil.SetupViewOptions ----@field show_hidden? boolean Show files and directories that start with "." ----@field is_hidden_file? fun(name: string, bufnr: integer): boolean This function defines what is considered a "hidden" file ----@field is_always_hidden? fun(name: string, bufnr: integer): boolean This function defines what will never be shown, even when `show_hidden` is set ----@field natural_order? boolean|"fast" Sort file names with numbers in a more intuitive order for humans. Can be slow for large directories. ----@field case_insensitive? boolean Sort file and directory names case insensitive ----@field sort? oil.SortSpec[] Sort order for the file list ----@field highlight_filename? fun(entry: oil.Entry, is_hidden: boolean, is_link_target: boolean, is_link_orphan: boolean): string|nil Customize the highlight group for the file name - ----@class (exact) oil.SortSpec ----@field [1] string ----@field [2] "asc"|"desc" - ----@class (exact) oil.GitOptions ----@field add fun(path: string): boolean ----@field mv fun(src_path: string, dest_path: string): boolean ----@field rm fun(path: string): boolean - ----@class (exact) oil.SetupGitOptions ----@field add? fun(path: string): boolean Return true to automatically git add a new file ----@field mv? fun(src_path: string, dest_path: string): boolean Return true to automatically git mv a moved file ----@field rm? fun(path: string): boolean Return true to automatically git rm a deleted file - ----@class (exact) oil.WindowDimensionDualConstraint ----@field [1] number ----@field [2] number - ----@alias oil.WindowDimension number|oil.WindowDimensionDualConstraint - ----@class (exact) oil.WindowConfig ----@field max_width oil.WindowDimension ----@field min_width oil.WindowDimension ----@field width? number ----@field max_height oil.WindowDimension ----@field min_height oil.WindowDimension ----@field height? number ----@field border string|string[] ----@field win_options table - ----@class (exact) oil.SetupWindowConfig ----@field max_width? oil.WindowDimension Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total" ----@field min_width? oil.WindowDimension Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. min_width = {40, 0.4} means "the greater of 40 columns or 40% of total" ----@field width? number Define an integer/float for the exact width of the preview window ----@field max_height? oil.WindowDimension Height dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. max_height = {80, 0.9} means "the lesser of 80 columns or 90% of total" ----@field min_height? oil.WindowDimension Height dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. min_height = {5, 0.1} means "the greater of 5 columns or 10% of total" ----@field height? number Define an integer/float for the exact height of the preview window ----@field border? string|string[] Window border ----@field win_options? table - ----@alias oil.PreviewMethod ----| '"load"' # Load the previewed file into a buffer ----| '"scratch"' # Put the text into a scratch buffer to avoid LSP attaching ----| '"fast_scratch"' # Put only the visible text into a scratch buffer - ----@class (exact) oil.PreviewWindowConfig ----@field update_on_cursor_moved boolean ----@field preview_method oil.PreviewMethod ----@field disable_preview fun(filename: string): boolean ----@field win_options table - ----@class (exact) oil.ConfirmationWindowConfig : oil.WindowConfig - ----@class (exact) oil.SetupPreviewWindowConfig ----@field update_on_cursor_moved? boolean Whether the preview window is automatically updated when the cursor is moved ----@field disable_preview? fun(filename: string): boolean A function that returns true to disable preview on a file e.g. to avoid lag ----@field preview_method? oil.PreviewMethod How to open the preview window ----@field win_options? table Window-local options to use for preview window buffers - ----@class (exact) oil.SetupConfirmationWindowConfig : oil.SetupWindowConfig - ----@class (exact) oil.ProgressWindowConfig : oil.WindowConfig ----@field minimized_border string|string[] - ----@class (exact) oil.SetupProgressWindowConfig : oil.SetupWindowConfig ----@field minimized_border? string|string[] The border for the minimized progress window - ----@class (exact) oil.FloatWindowConfig ----@field padding integer ----@field max_width integer ----@field max_height integer ----@field border string|string[] ----@field win_options table ----@field get_win_title fun(winid: integer): string ----@field preview_split "auto"|"left"|"right"|"above"|"below" ----@field override fun(conf: table): table - ----@class (exact) oil.SetupFloatWindowConfig ----@field padding? integer ----@field max_width? integer ----@field max_height? integer ----@field border? string|string[] Window border ----@field win_options? table ----@field get_win_title? fun(winid: integer): string ----@field preview_split? "auto"|"left"|"right"|"above"|"below" Direction that the preview command will split the window ----@field override? fun(conf: table): table - ----@class (exact) oil.SimpleWindowConfig ----@field border string|string[] - ----@class (exact) oil.SetupSimpleWindowConfig ----@field border? string|string[] Window border - M.setup = function(opts) - opts = opts or {} - - local new_conf = vim.tbl_deep_extend("keep", opts, default_config) + local new_conf = vim.tbl_deep_extend("keep", opts or {}, default_config) if not new_conf.use_default_keymaps then new_conf.keymaps = opts.keymaps or {} - elseif opts.keymaps then - -- We don't want to deep merge the keymaps, we want any keymap defined by the user to override - -- everything about the default. - for k, v in pairs(opts.keymaps) do - new_conf.keymaps[k] = v + end + + if new_conf.delete_to_trash then + local trash_bin = vim.split(new_conf.trash_command, " ")[1] + if vim.fn.executable(trash_bin) == 0 then + vim.notify( + string.format( + "oil.nvim: delete_to_trash is true, but '%s' executable not found.\nDeleted files will be permanently removed.", + new_conf.trash_command + ), + vim.log.levels.WARN + ) + new_conf.delete_to_trash = false end end - -- Backwards compatibility for old versions that don't support winborder - if vim.fn.has("nvim-0.11") == 0 then - new_conf = vim.tbl_deep_extend("keep", new_conf, { - float = { border = "rounded" }, - confirmation = { border = "rounded" }, - progress = { border = "rounded" }, - ssh = { border = "rounded" }, - keymaps_help = { border = "rounded" }, - }) - end - - -- Backwards compatibility. We renamed the 'preview' window config to be called 'confirmation'. - if opts.preview and not opts.confirmation then - new_conf.confirmation = vim.tbl_deep_extend("keep", opts.preview, default_config.confirmation) - end - -- Backwards compatibility. We renamed the 'preview' config to 'preview_win' - if opts.preview and opts.preview.update_on_cursor_moved ~= nil then - new_conf.preview_win.update_on_cursor_moved = opts.preview.update_on_cursor_moved - end - - if new_conf.lsp_rename_autosave ~= nil then - new_conf.lsp_file_methods.autosave_changes = new_conf.lsp_rename_autosave - new_conf.lsp_rename_autosave = nil - vim.notify_once( - "oil config value lsp_rename_autosave has moved to lsp_file_methods.autosave_changes.\nCompatibility will be removed on 2024-09-01.", - vim.log.levels.WARN - ) - end - - -- This option was renamed because it is no longer experimental - if new_conf.experimental_watch_for_changes then - new_conf.watch_for_changes = true - end - for k, v in pairs(new_conf) do M[k] = v end @@ -456,6 +165,46 @@ M.setup = function(opts) M.adapter_to_scheme[v] = k end M._adapter_by_scheme = {} + if type(M.trash) == "string" then + M.trash = vim.fn.fnamemodify(vim.fn.expand(M.trash), ":p") + end +end + +---@return nil|string +M.get_trash_url = function() + if not M.trash then + return nil + end + local fs = require("oil.fs") + if M.trash == true then + local data_home = os.getenv("XDG_DATA_HOME") or vim.fn.expand("~/.local/share") + local preferred = fs.join(data_home, "trash") + local candidates = { + preferred, + } + if fs.is_windows then + -- TODO permission issues when using the recycle bin. The folder gets created without + -- read/write perms, so all operations fail + -- local cwd = vim.fn.getcwd() + -- table.insert(candidates, 1, cwd:sub(1, 3) .. "$Recycle.Bin") + -- table.insert(candidates, 1, "C:\\$Recycle.Bin") + else + table.insert(candidates, fs.join(data_home, "Trash", "files")) + table.insert(candidates, fs.join(os.getenv("HOME"), ".Trash")) + end + local trash_dir = preferred + for _, candidate in ipairs(candidates) do + if vim.fn.isdirectory(candidate) == 1 then + trash_dir = candidate + break + end + end + + local oil_trash_dir = vim.fn.fnamemodify(fs.join(trash_dir, "nvim", "oil"), ":p") + fs.mkdirp(oil_trash_dir) + M.trash = oil_trash_dir + end + return M.adapter_to_scheme.files .. fs.os_to_posix_path(M.trash) end ---@param scheme nil|string @@ -476,6 +225,10 @@ M.get_adapter_by_scheme = function(scheme) if adapter == nil then local name = M.adapters[scheme] if not name then + vim.notify( + string.format("Could not find oil adapter for scheme '%s'", scheme), + vim.log.levels.ERROR + ) return nil end local ok @@ -486,6 +239,7 @@ M.get_adapter_by_scheme = function(scheme) else M._adapter_by_scheme[scheme] = false adapter = false + vim.notify(string.format("Could not find oil adapter '%s'", name), vim.log.levels.ERROR) end end if adapter then diff --git a/lua/oil/constants.lua b/lua/oil/constants.lua index 3f5a38a..e1ef56b 100644 --- a/lua/oil/constants.lua +++ b/lua/oil/constants.lua @@ -2,7 +2,7 @@ local M = {} ---Store entries as a list-like table for maximum space efficiency and retrieval speed. ---We use the constants below to index into the table. ----@alias oil.InternalEntry {[1]: integer, [2]: string, [3]: oil.EntryType, [4]: nil|table} +---@alias oil.InternalEntry any[] -- Indexes into oil.InternalEntry M.FIELD_ID = 1 diff --git a/lua/oil/fs.lua b/lua/oil/fs.lua index f169c0b..7c9002a 100644 --- a/lua/oil/fs.lua +++ b/lua/oil/fs.lua @@ -1,14 +1,9 @@ -local log = require("oil.log") local M = {} -local uv = vim.uv or vim.loop - ---@type boolean -M.is_windows = uv.os_uname().version:match("Windows") +M.is_windows = vim.loop.os_uname().version:match("Windows") -M.is_mac = uv.os_uname().sysname == "Darwin" - -M.is_linux = not M.is_windows and not M.is_mac +M.is_mac = vim.loop.os_uname().sysname == "Darwin" ---@type string M.sep = M.is_windows and "\\" or "/" @@ -29,69 +24,27 @@ M.is_absolute = function(dir) end end -M.abspath = function(path) - if not M.is_absolute(path) then - path = vim.fn.fnamemodify(path, ":p") - end - return path -end - ---@param path string ---@param cb fun(err: nil|string) M.touch = function(path, cb) - uv.fs_open(path, "a", 420, function(err, fd) -- 0644 + vim.loop.fs_open(path, "a", 420, function(err, fd) -- 0644 if err then cb(err) else - assert(fd) - uv.fs_close(fd, cb) + vim.loop.fs_close(fd, cb) end end) end ---- Returns true if candidate is a subpath of root, or if they are the same path. ----@param root string ----@param candidate string ----@return boolean -M.is_subpath = function(root, candidate) - if candidate == "" then - return false - end - root = vim.fs.normalize(M.abspath(root)) - -- Trim trailing "/" from the root - if root:find("/", -1) then - root = root:sub(1, -2) - end - candidate = vim.fs.normalize(M.abspath(candidate)) - if M.is_windows then - root = root:lower() - candidate = candidate:lower() - end - if root == candidate then - return true - end - local prefix = candidate:sub(1, root:len()) - if prefix ~= root then - return false - end - - 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() - - return candidate_starts_with_sep or root_ends_with_sep -end - ---@param path string ---@return string M.posix_to_os_path = function(path) if M.is_windows then if vim.startswith(path, "/") then - local drive = path:match("^/(%a+)") - local rem = path:sub(drive:len() + 2) - return string.format("%s:%s", drive, rem:gsub("/", "\\")) + local drive, rem = path:match("^/([^/]+)/(.*)$") + return string.format("%s:\\%s", drive, rem:gsub("/", "\\")) else - local newpath = path:gsub("/", "\\") - return newpath + return path:gsub("/", "\\") end else return path @@ -106,48 +59,33 @@ M.os_to_posix_path = function(path) local drive, rem = path:match("^([^:]+):\\(.*)$") return string.format("/%s/%s", drive:upper(), rem:gsub("\\", "/")) else - local newpath = path:gsub("\\", "/") - return newpath + return path:gsub("\\", "/") end else return path end end -local home_dir = assert(uv.os_homedir()) +local home_dir = vim.loop.os_homedir() ---@param path string ----@param relative_to? string Shorten relative to this path (default cwd) ---@return string -M.shorten_path = function(path, relative_to) - if not relative_to then - relative_to = vim.fn.getcwd() - end - local relpath - if M.is_subpath(relative_to, path) then - local idx = relative_to:len() + 1 - -- Trim the dividing slash if it's not included in relative_to - if not vim.endswith(relative_to, "/") and not vim.endswith(relative_to, "\\") then - idx = idx + 1 - end - relpath = path:sub(idx) - if relpath == "" then - relpath = "." +M.shorten_path = function(path) + local cwd = vim.fn.getcwd() + if vim.startswith(path, cwd) then + local relative = path:sub(cwd:len() + 2) + if relative == "" then + relative = "." end + return relative end - if M.is_subpath(home_dir, path) then - local homepath = "~" .. path:sub(home_dir:len() + 1) - if not relpath or homepath:len() < relpath:len() then - return homepath - end + if vim.startswith(path, home_dir) then + return "~" .. path:sub(home_dir:len() + 1) end - return relpath or path + return path end ----@param dir string ----@param mode? integer -M.mkdirp = function(dir, mode) - mode = mode or 493 +M.mkdirp = function(dir) local mod = "" local path = dir while vim.fn.isdirectory(path) == 0 do @@ -157,32 +95,30 @@ M.mkdirp = function(dir, mode) while mod ~= "" do mod = mod:sub(3) path = vim.fn.fnamemodify(dir, mod) - uv.fs_mkdir(path, mode) + vim.loop.fs_mkdir(path, 493) end end ---@param dir string ---@param cb fun(err: nil|string, entries: nil|{type: oil.EntryType, name: string}) M.listdir = function(dir, cb) - ---@diagnostic disable-next-line: param-type-mismatch, discard-returns - uv.fs_opendir(dir, function(open_err, fd) + vim.loop.fs_opendir(dir, function(open_err, fd) if open_err then return cb(open_err) end local read_next read_next = function() - uv.fs_readdir(fd, function(err, entries) + vim.loop.fs_readdir(fd, function(err, entries) if err then - uv.fs_closedir(fd, function() + vim.loop.fs_closedir(fd, function() cb(err) end) return elseif entries then - ---@diagnostic disable-next-line: param-type-mismatch cb(nil, entries) read_next() else - uv.fs_closedir(fd, function(close_err) + vim.loop.fs_closedir(fd, function(close_err) if close_err then cb(close_err) else @@ -193,8 +129,7 @@ M.listdir = function(dir, cb) end) end read_next() - ---@diagnostic disable-next-line: param-type-mismatch - end, 10000) + end, 100) -- TODO do some testing for this end ---@param entry_type oil.EntryType @@ -202,23 +137,22 @@ end ---@param cb fun(err: nil|string) M.recursive_delete = function(entry_type, path, cb) if entry_type ~= "directory" then - return uv.fs_unlink(path, cb) + return vim.loop.fs_unlink(path, cb) end - ---@diagnostic disable-next-line: param-type-mismatch, discard-returns - uv.fs_opendir(path, function(open_err, fd) + vim.loop.fs_opendir(path, function(open_err, fd) if open_err then return cb(open_err) end local poll poll = function(inner_cb) - uv.fs_readdir(fd, function(err, entries) + vim.loop.fs_readdir(fd, function(err, entries) if err then return inner_cb(err) elseif entries then local waiting = #entries local complete complete = function(err2) - if err2 then + if err then complete = function() end return inner_cb(err2) end @@ -236,91 +170,55 @@ M.recursive_delete = function(entry_type, path, cb) end) end poll(function(err) - uv.fs_closedir(fd) + vim.loop.fs_closedir(fd) if err then return cb(err) end - uv.fs_rmdir(path, cb) + vim.loop.fs_rmdir(path, cb) end) - ---@diagnostic disable-next-line: param-type-mismatch - end, 10000) + end, 100) -- TODO do some testing for this end ----Move the undofile for the file at src_path to dest_path ----@param src_path string ----@param dest_path string ----@param copy boolean -local move_undofile = vim.schedule_wrap(function(src_path, dest_path, copy) - local undofile = vim.fn.undofile(src_path) - uv.fs_stat( - undofile, - vim.schedule_wrap(function(stat_err) - if stat_err then - -- undofile doesn't exist - return - end - local dest_undofile = vim.fn.undofile(dest_path) - if copy then - uv.fs_copyfile(src_path, dest_path, function(err) - if err then - log.warn("Error copying undofile %s: %s", undofile, err) - end - end) - else - uv.fs_rename(undofile, dest_undofile, function(err) - if err then - log.warn("Error moving undofile %s: %s", undofile, err) - end - end) - end - end) - ) -end) - ---@param entry_type oil.EntryType ---@param src_path string ---@param dest_path string ---@param cb fun(err: nil|string) M.recursive_copy = function(entry_type, src_path, dest_path, cb) if entry_type == "link" then - uv.fs_readlink(src_path, function(link_err, link) + vim.loop.fs_readlink(src_path, function(link_err, link) if link_err then return cb(link_err) end - assert(link) - uv.fs_symlink(link, dest_path, 0, cb) + vim.loop.fs_symlink(link, dest_path, nil, cb) end) return end if entry_type ~= "directory" then - uv.fs_copyfile(src_path, dest_path, { excl = true }, cb) - move_undofile(src_path, dest_path, true) + vim.loop.fs_copyfile(src_path, dest_path, { excl = true }, cb) return end - uv.fs_stat(src_path, function(stat_err, src_stat) + vim.loop.fs_stat(src_path, function(stat_err, src_stat) if stat_err then return cb(stat_err) end - assert(src_stat) - uv.fs_mkdir(dest_path, src_stat.mode, function(mkdir_err) + vim.loop.fs_mkdir(dest_path, src_stat.mode, function(mkdir_err) if mkdir_err then return cb(mkdir_err) end - ---@diagnostic disable-next-line: param-type-mismatch, discard-returns - uv.fs_opendir(src_path, function(open_err, fd) + vim.loop.fs_opendir(src_path, function(open_err, fd) if open_err then return cb(open_err) end local poll poll = function(inner_cb) - uv.fs_readdir(fd, function(err, entries) + vim.loop.fs_readdir(fd, function(err, entries) if err then return inner_cb(err) elseif entries then local waiting = #entries local complete complete = function(err2) - if err2 then + if err then complete = function() end return inner_cb(err2) end @@ -343,8 +241,7 @@ M.recursive_copy = function(entry_type, src_path, dest_path, cb) end) end poll(cb) - ---@diagnostic disable-next-line: param-type-mismatch - end, 10000) + end, 100) -- TODO do some testing for this end) end) end @@ -354,7 +251,7 @@ end ---@param dest_path string ---@param cb fun(err: nil|string) M.recursive_move = function(entry_type, src_path, dest_path, cb) - uv.fs_rename(src_path, dest_path, function(err) + vim.loop.fs_rename(src_path, dest_path, function(err) if err then -- fs_rename fails for cross-partition or cross-device operations. -- We then fall back to a copy + delete @@ -366,9 +263,6 @@ M.recursive_move = function(entry_type, src_path, dest_path, cb) end end) else - if entry_type ~= "directory" then - move_undofile(src_path, dest_path, false) - end cb() end end) diff --git a/lua/oil/git.lua b/lua/oil/git.lua deleted file mode 100644 index ec3b84b..0000000 --- a/lua/oil/git.lua +++ /dev/null @@ -1,118 +0,0 @@ --- integration with git operations -local fs = require("oil.fs") - -local M = {} - ----@param path string ----@return string|nil -M.get_root = function(path) - local git_dir = vim.fs.find(".git", { upward = true, path = path })[1] - if git_dir then - return vim.fs.dirname(git_dir) - else - return nil - end -end - ----@param path string ----@param cb fun(err: nil|string) -M.add = function(path, cb) - local root = M.get_root(path) - if not root then - return cb() - end - - local stderr = "" - local jid = vim.fn.jobstart({ "git", "add", path }, { - cwd = root, - stderr_buffered = true, - on_stderr = function(_, data) - stderr = table.concat(data, "\n") - end, - on_exit = function(_, code) - if code ~= 0 then - cb("Error in git add: " .. stderr) - else - cb() - end - end, - }) - if jid <= 0 then - cb() - end -end - ----@param path string ----@param cb fun(err: nil|string) -M.rm = function(path, cb) - local root = M.get_root(path) - if not root then - return cb() - end - - local stderr = "" - local jid = vim.fn.jobstart({ "git", "rm", "-r", path }, { - cwd = root, - stderr_buffered = true, - on_stderr = function(_, data) - stderr = table.concat(data, "\n") - end, - on_exit = function(_, code) - if code ~= 0 then - stderr = vim.trim(stderr) - if stderr:match("^fatal: pathspec '.*' did not match any files$") then - cb() - else - cb("Error in git rm: " .. stderr) - end - else - cb() - end - end, - }) - if jid <= 0 then - cb() - end -end - ----@param entry_type oil.EntryType ----@param src_path string ----@param dest_path string ----@param cb fun(err: nil|string) -M.mv = function(entry_type, src_path, dest_path, cb) - local src_git = M.get_root(src_path) - if not src_git or src_git ~= M.get_root(dest_path) then - fs.recursive_move(entry_type, src_path, dest_path, cb) - return - end - - local stderr = "" - local jid = vim.fn.jobstart({ "git", "mv", src_path, dest_path }, { - cwd = src_git, - stderr_buffered = true, - on_stderr = function(_, data) - stderr = table.concat(data, "\n") - end, - on_exit = function(_, code) - if code ~= 0 then - stderr = vim.trim(stderr) - if - stderr:match("^fatal: not under version control") - or stderr:match("^fatal: source directory is empty") - then - fs.recursive_move(entry_type, src_path, dest_path, cb) - else - cb("Error in git mv: " .. stderr) - end - else - cb() - end - end, - }) - if jid <= 0 then - -- Failed to run git, fall back to normal filesystem operations - fs.recursive_move(entry_type, src_path, dest_path, cb) - end -end - -return M diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 908d6dd..ebe655e 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -1,34 +1,26 @@ local M = {} ----@class (exact) oil.Entry +---@class oil.Entry ---@field name string ---@field type oil.EntryType ---@field id nil|integer Will be nil if it hasn't been persisted to disk yet ---@field parsed_name nil|string ----@field meta nil|table ----@alias oil.EntryType uv.aliases.fs_types ----@alias oil.HlRange { [1]: string, [2]: integer, [3]: integer } A tuple of highlight group name, col_start, col_end ----@alias oil.HlTuple { [1]: string, [2]: string } A tuple of text, highlight group ----@alias oil.HlRangeTuple { [1]: string, [2]: oil.HlRange[] } A tuple of text, internal highlights ----@alias oil.TextChunk string|oil.HlTuple|oil.HlRangeTuple ----@alias oil.CrossAdapterAction "copy"|"move" +---@alias oil.EntryType "file"|"directory"|"socket"|"link" +---@alias oil.TextChunk string|string[] ----@class (exact) oil.Adapter ----@field name string The unique name of the adapter (this will be set automatically) ----@field list fun(path: string, column_defs: string[], cb: fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())) Async function to list a directory. ----@field is_modifiable fun(bufnr: integer): boolean Return true if this directory is modifiable (allows for directories with read-only permissions). ----@field get_column fun(name: string): nil|oil.ColumnDefinition If the adapter has any adapter-specific columns, return them when fetched by name. ----@field get_parent? fun(bufname: string): string Get the parent url of the given buffer ----@field normalize_url fun(url: string, callback: fun(url: string)) Before oil opens a url it will be normalized. This allows for link following, path normalizing, and converting an oil file url to the actual path of a file. ----@field get_entry_path? fun(url: string, entry: oil.Entry, callback: fun(path: string)) Similar to normalize_url, but used when selecting an entry ----@field render_action? fun(action: oil.Action): string Render a mutation action for display in the preview window. Only needed if adapter is modifiable. ----@field perform_action? fun(action: oil.Action, cb: fun(err: nil|string)) Perform a mutation action. Only needed if adapter is modifiable. ----@field read_file? fun(bufnr: integer) Used for adapters that deal with remote/virtual files. Read the contents of the file into a buffer. ----@field write_file? fun(bufnr: integer) Used for adapters that deal with remote/virtual files. Write the contents of a buffer to the destination. ----@field supported_cross_adapter_actions? table Mapping of adapter name to enum for all other adapters that can be used as a src or dest for move/copy actions. ----@field filter_action? fun(action: oil.Action): boolean When present, filter out actions as they are created ----@field filter_error? fun(action: oil.ParseError): boolean When present, filter out errors from parsing a buffer +---@class oil.Adapter +---@field name string +---@field list fun(path: string, cb: fun(err: nil|string, entries: nil|oil.InternalEntry[])) +---@field is_modifiable fun(bufnr: integer): boolean +---@field get_column fun(name: string): nil|oil.ColumnDefinition +---@field normalize_url fun(url: string, callback: fun(url: string)) +---@field get_parent nil|fun(bufname: string): string +---@field supports_xfer nil|table +---@field render_action nil|fun(action: oil.Action): string +---@field perform_action nil|fun(action: oil.Action, cb: fun(err: nil|string)) +---@field read_file fun(bufnr: integer) +---@field write_file fun(bufnr: integer) ---Get the entry on a specific line (1-indexed) ---@param bufnr integer @@ -59,7 +51,6 @@ M.get_entry_on_line = function(bufnr, lnum) return entry else return { - id = result.data.id, name = result.data.name, type = result.data._type, parsed_name = result.data.name, @@ -114,22 +105,41 @@ M.discard_all_changes = function() end end +---Delete all files in the trash directory +---@private +---@note +--- Trash functionality is incomplete and experimental. +M.empty_trash = function() + local config = require("oil.config") + local fs = require("oil.fs") + local util = require("oil.util") + local trash_url = config.get_trash_url() + if not trash_url then + vim.notify("No trash directory configured", vim.log.levels.WARN) + return + end + local _, path = util.parse_url(trash_url) + local dir = fs.posix_to_os_path(path) + if vim.fn.isdirectory(dir) == 1 then + fs.recursive_delete("directory", dir, function(err) + if err then + vim.notify(string.format("Error emptying trash: %s", err), vim.log.levels.ERROR) + else + vim.notify("Trash emptied") + fs.mkdirp(dir) + end + end) + end +end + ---Change the display columns for oil ---@param cols oil.ColumnSpec[] M.set_columns = function(cols) require("oil.view").set_columns(cols) end ----Change the sort order for oil ----@param sort oil.SortSpec[] List of columns plus direction. See :help oil-columns to see which ones are sortable. ----@example ---- require("oil").set_sort({ { "type", "asc" }, { "size", "desc" } }) -M.set_sort = function(sort) - require("oil.view").set_sort(sort) -end - ---Change how oil determines if the file is hidden ----@param is_hidden_file fun(filename: string, bufnr: integer): boolean Return true if the file/dir should be hidden +---@param is_hidden_file fun(filename: string, bufnr: nil|integer): boolean Return true if the file/dir should be hidden M.set_is_hidden_file = function(is_hidden_file) require("oil.view").set_is_hidden_file(is_hidden_file) end @@ -140,16 +150,13 @@ M.toggle_hidden = function() end ---Get the current directory ----@param bufnr? integer ---@return nil|string -M.get_current_dir = function(bufnr) +M.get_current_dir = function() local config = require("oil.config") local fs = require("oil.fs") local util = require("oil.util") - local buf_name = vim.api.nvim_buf_get_name(bufnr or 0) - local scheme, path = util.parse_url(buf_name) + local scheme, path = util.parse_url(vim.api.nvim_buf_get_name(0)) if config.adapters[scheme] == "files" then - assert(path) return fs.posix_to_os_path(path) end end @@ -157,13 +164,9 @@ end ---Get the oil url for a given directory ---@private ---@param dir nil|string When nil, use the cwd ----@param use_oil_parent nil|boolean If in an oil buffer, return the parent (default true) ----@return string The parent url +---@return nil|string The parent url ---@return nil|string The basename (if present) of the file/dir we were just in -M.get_url_for_path = function(dir, use_oil_parent) - if use_oil_parent == nil then - use_oil_parent = true - end +M.get_url_for_path = function(dir) local config = require("oil.config") local fs = require("oil.fs") local util = require("oil.util") @@ -180,16 +183,15 @@ M.get_url_for_path = function(dir, use_oil_parent) return config.adapter_to_scheme.files .. path else local bufname = vim.api.nvim_buf_get_name(0) - return M.get_buffer_parent_url(bufname, use_oil_parent) + return M.get_buffer_parent_url(bufname) end end ---@private ---@param bufname string ----@param use_oil_parent boolean If in an oil buffer, return the parent ---@return string ---@return nil|string -M.get_buffer_parent_url = function(bufname, use_oil_parent) +M.get_buffer_parent_url = function(bufname) local config = require("oil.config") local fs = require("oil.fs") local pathutil = require("oil.pathutil") @@ -207,22 +209,13 @@ M.get_buffer_parent_url = function(bufname, use_oil_parent) local parent_url = util.addslash(scheme .. parent) return parent_url, basename else - assert(path) + -- TODO maybe we should remove this special case and turn it into a config if scheme == "term://" then - ---@type string - path = vim.fn.expand(path:match("^(.*)//")) ---@diagnostic disable-line: assign-type-mismatch + path = vim.fn.expand(path:match("^(.*)//")) return config.adapter_to_scheme.files .. util.addslash(path) end - -- This is some unknown buffer scheme - if not config.adapters[scheme] then - return vim.fn.getcwd() - end - - if not use_oil_parent then - return bufname - end - local adapter = assert(config.get_adapter_by_scheme(scheme)) + local adapter = config.get_adapter_by_scheme(scheme) local parent_url if adapter and adapter.get_parent then local adapter_scheme = config.adapter_to_scheme[adapter.name] @@ -239,33 +232,52 @@ M.get_buffer_parent_url = function(bufname, use_oil_parent) end end ----@class (exact) oil.OpenOpts ----@field preview? oil.OpenPreviewOpts When present, open the preview window after opening oil - ---Open oil browser in a floating window ----@param dir? string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file ----@param opts? oil.OpenOpts ----@param cb? fun() Called after the oil buffer is ready -M.open_float = function(dir, opts, cb) - opts = opts or {} +---@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file +M.open_float = function(dir) local config = require("oil.config") local layout = require("oil.layout") local util = require("oil.util") local view = require("oil.view") - local parent_url, basename = M.get_url_for_path(dir) + if not parent_url then + return + end if basename then view.set_last_cursor(parent_url, basename) end local bufnr = vim.api.nvim_create_buf(false, true) vim.bo[bufnr].bufhidden = "wipe" - local win_opts = layout.get_fullscreen_win_opts() + local total_width = vim.o.columns + local total_height = layout.get_editor_height() + local width = total_width - 2 * config.float.padding + if config.float.border ~= "none" then + width = width - 2 -- The border consumes 1 col on each side + end + if config.float.max_width > 0 then + width = math.min(width, config.float.max_width) + end + local height = total_height - 2 * config.float.padding + if config.float.max_height > 0 then + height = math.min(height, config.float.max_height) + end + local row = math.floor((total_height - height) / 2) + local col = math.floor((total_width - width) / 2) - 1 -- adjust for border width + local win_opts = { + relative = "editor", + width = width, + height = height, + row = row, + col = col, + style = "minimal", + border = config.float.border, + zindex = 45, + } + win_opts = config.float.override(win_opts) or win_opts - local original_winid = vim.api.nvim_get_current_win() local winid = vim.api.nvim_open_win(bufnr, true, win_opts) vim.w[winid].is_oil_win = true - vim.w[winid].oil_original_win = original_winid for k, v in pairs(config.float.win_options) do vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) end @@ -276,7 +288,7 @@ M.open_float = function(dir, opts, cb) desc = "Close floating oil window", group = "Oil", callback = vim.schedule_wrap(function() - if util.is_floating_win() or vim.fn.win_gettype() == "command" then + if util.is_floating_win() then return end if vim.api.nvim_win_is_valid(winid) then @@ -291,49 +303,42 @@ M.open_float = function(dir, opts, cb) }) ) - table.insert( - autocmds, - vim.api.nvim_create_autocmd("BufWinEnter", { - desc = "Reset local oil window options when buffer changes", - pattern = "*", - callback = function(params) - local winbuf = params.buf - if not vim.api.nvim_win_is_valid(winid) or vim.api.nvim_win_get_buf(winid) ~= winbuf then - return - end - for k, v in pairs(config.float.win_options) do - vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) - end - - -- Update the floating window title - if vim.fn.has("nvim-0.9") == 1 and config.float.border ~= "none" then - local cur_win_opts = vim.api.nvim_win_get_config(winid) + -- Update the window title when we switch buffers + if vim.fn.has("nvim-0.9") == 1 and config.float.border ~= "none" then + local function get_title() + local src_buf = vim.api.nvim_win_get_buf(winid) + local title = vim.api.nvim_buf_get_name(src_buf) + local scheme, path = util.parse_url(title) + if config.adapters[scheme] == "files" then + local fs = require("oil.fs") + title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~") + end + return title + end + table.insert( + autocmds, + vim.api.nvim_create_autocmd("BufWinEnter", { + desc = "Update oil floating window title when buffer changes", + pattern = "*", + callback = function(params) + local winbuf = params.buf + if not vim.api.nvim_win_is_valid(winid) or vim.api.nvim_win_get_buf(winid) ~= winbuf then + return + end vim.api.nvim_win_set_config(winid, { relative = "editor", - row = cur_win_opts.row, - col = cur_win_opts.col, - width = cur_win_opts.width, - height = cur_win_opts.height, - title = util.get_title(winid), + row = win_opts.row, + col = win_opts.col, + width = win_opts.width, + height = win_opts.height, + title = get_title(), }) - end - end, - }) - ) - - vim.cmd.edit({ args = { util.escape_filename(parent_url) }, mods = { keepalt = true } }) - -- :edit will set buflisted = true, but we may not want that - if config.buf_options.buflisted ~= nil then - vim.api.nvim_set_option_value("buflisted", config.buf_options.buflisted, { buf = 0 }) + end, + }) + ) end - util.run_after_load(0, function() - if opts.preview then - M.open_preview(opts.preview, cb) - elseif cb then - cb() - end - end) + vim.cmd.edit({ args = { util.escape_filename(parent_url) }, mods = { keepalt = true } }) if vim.fn.has("nvim-0.9") == 0 then util.add_title_to_win(winid) @@ -342,81 +347,34 @@ end ---Open oil browser in a floating window, or close it if open ---@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file ----@param opts? oil.OpenOpts ----@param cb? fun() Called after the oil buffer is ready -M.toggle_float = function(dir, opts, cb) - if vim.w.is_oil_win then - M.close() - if cb then - cb() - end +M.toggle_float = function(dir) + local util = require("oil.util") + if util.is_oil_bufnr(0) and util.is_floating_win(0) then + vim.api.nvim_win_close(0, true) else - M.open_float(dir, opts, cb) + M.open_float(dir) end end ----@param oil_bufnr? integer -local function update_preview_window(oil_bufnr) - oil_bufnr = oil_bufnr or 0 - local util = require("oil.util") - util.run_after_load(oil_bufnr, function() - local cursor_entry = M.get_cursor_entry() - local preview_win_id = util.get_preview_win() - if - cursor_entry - and preview_win_id - and cursor_entry.id ~= vim.w[preview_win_id].oil_entry_id - then - M.open_preview() - end - end) -end - ---Open oil browser for a directory ----@param dir? string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file ----@param opts? oil.OpenOpts ----@param cb? fun() Called after the oil buffer is ready -M.open = function(dir, opts, cb) - opts = opts or {} - local config = require("oil.config") +---@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file +M.open = function(dir) local util = require("oil.util") local view = require("oil.view") local parent_url, basename = M.get_url_for_path(dir) + if not parent_url then + return + end if basename then view.set_last_cursor(parent_url, basename) end vim.cmd.edit({ args = { util.escape_filename(parent_url) }, mods = { keepalt = true } }) - -- :edit will set buflisted = true, but we may not want that - if config.buf_options.buflisted ~= nil then - vim.api.nvim_set_option_value("buflisted", config.buf_options.buflisted, { buf = 0 }) - end - - util.run_after_load(0, function() - if opts.preview then - M.open_preview(opts.preview, cb) - elseif cb then - cb() - end - end) - - -- If preview window exists, update its content - update_preview_window() end ----@class oil.CloseOpts ----@field exit_if_last_buf? boolean Exit vim if this oil buffer is the last open buffer - ---Restore the buffer that was present when oil was opened ----@param opts? oil.CloseOpts -M.close = function(opts) - opts = opts or {} - -- If we're in a floating oil window, close it and try to restore focus to the original window +M.close = function() if vim.w.is_oil_win then - local original_winid = vim.w.oil_original_win vim.api.nvim_win_close(0, true) - if original_winid and vim.api.nvim_win_is_valid(original_winid) then - vim.api.nvim_set_current_win(original_winid) - end return end local ok, bufnr = pcall(vim.api.nvim_win_get_var, 0, "oil_original_buffer") @@ -432,215 +390,26 @@ M.close = function(opts) -- buffer first local oilbuf = vim.api.nvim_get_current_buf() ok = pcall(vim.cmd.bprev) - -- If `bprev` failed, there are no buffers open if not ok then - -- either exit or create a new blank buffer - if opts.exit_if_last_buf then - vim.cmd.quit() - else - vim.cmd.enew() - end + -- If `bprev` failed, there are no buffers open so we should create a new one with enew + vim.cmd.enew() end vim.api.nvim_buf_delete(oilbuf, { force = true }) end ----@class oil.OpenPreviewOpts ----@field vertical? boolean Open the buffer in a vertical split ----@field horizontal? boolean Open the buffer in a horizontal split ----@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier - ----Preview the entry under the cursor in a split ----@param opts? oil.OpenPreviewOpts ----@param callback? fun(err: nil|string) Called once the preview window has been opened -M.open_preview = function(opts, callback) - opts = opts or {} - local config = require("oil.config") - local layout = require("oil.layout") - local util = require("oil.util") - - local function finish(err) - if err then - vim.notify(err, vim.log.levels.ERROR) - end - if callback then - callback(err) - end - end - - if not opts.horizontal and opts.vertical == nil then - opts.vertical = true - end - if not opts.split then - if opts.horizontal then - opts.split = vim.o.splitbelow and "belowright" or "aboveleft" - else - opts.split = vim.o.splitright and "belowright" or "aboveleft" - end - end - - local preview_win = util.get_preview_win({ include_not_owned = true }) - local prev_win = vim.api.nvim_get_current_win() - local bufnr = vim.api.nvim_get_current_buf() - - local entry = M.get_cursor_entry() - if not entry then - return finish("Could not find entry under cursor") - end - local entry_title = entry.name - if entry.type == "directory" then - entry_title = entry_title .. "/" - end - - if util.is_floating_win() then - if preview_win == nil then - local root_win_opts, preview_win_opts = - layout.split_window(0, config.float.preview_split, config.float.padding) - - local win_opts_oil = { - relative = "editor", - width = root_win_opts.width, - height = root_win_opts.height, - row = root_win_opts.row, - col = root_win_opts.col, - border = config.float.border, - zindex = 45, - } - vim.api.nvim_win_set_config(0, win_opts_oil) - local win_opts = { - relative = "editor", - width = preview_win_opts.width, - height = preview_win_opts.height, - row = preview_win_opts.row, - col = preview_win_opts.col, - border = config.float.border, - zindex = 45, - focusable = false, - noautocmd = true, - style = "minimal", - } - - if vim.fn.has("nvim-0.9") == 1 then - win_opts.title = entry_title - end - - preview_win = vim.api.nvim_open_win(bufnr, true, win_opts) - vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = preview_win }) - vim.api.nvim_win_set_var(preview_win, "oil_preview", true) - vim.api.nvim_set_current_win(prev_win) - elseif vim.fn.has("nvim-0.9") == 1 then - vim.api.nvim_win_set_config(preview_win, { title = entry_title }) - end - end - - local cmd = preview_win and "buffer" or "sbuffer" - local mods = { - vertical = opts.vertical, - horizontal = opts.horizontal, - split = opts.split, - } - - -- HACK Switching windows takes us out of visual mode. - -- Switching with nvim_set_current_win causes the previous visual selection (as used by `gv`) to - -- not get set properly. So we have to switch windows this way instead. - local hack_set_win = function(winid) - local winnr = vim.api.nvim_win_get_number(winid) - vim.cmd.wincmd({ args = { "w" }, count = winnr }) - end - - util.get_edit_path(bufnr, entry, function(normalized_url) - local mc = package.loaded["multicursor-nvim"] - local has_multicursors = mc and mc.hasCursors() - local is_visual_mode = util.is_visual_mode() - if preview_win then - if is_visual_mode then - hack_set_win(preview_win) - else - vim.api.nvim_set_current_win(preview_win) - end - end - - local entry_is_file = not vim.endswith(normalized_url, "/") - local filebufnr - if entry_is_file then - if config.preview_win.disable_preview(normalized_url) then - filebufnr = vim.api.nvim_create_buf(false, true) - vim.bo[filebufnr].bufhidden = "wipe" - vim.bo[filebufnr].buftype = "nofile" - util.render_text(filebufnr, "Preview disabled", { winid = preview_win }) - elseif - config.preview_win.preview_method ~= "load" - and not util.file_matches_bufreadcmd(normalized_url) - then - filebufnr = - util.read_file_to_scratch_buffer(normalized_url, config.preview_win.preview_method) - end - end - - if not filebufnr then - filebufnr = vim.fn.bufadd(normalized_url) - if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then - vim.bo[filebufnr].bufhidden = "wipe" - vim.b[filebufnr].oil_preview_buffer = true - end - end - - ---@diagnostic disable-next-line: param-type-mismatch - local ok, err = pcall(vim.cmd, { - cmd = cmd, - args = { filebufnr }, - mods = mods, - }) - -- Ignore swapfile errors - if not ok and err and not err:match("^Vim:E325:") then - vim.api.nvim_echo({ { err, "Error" } }, true, {}) - end - - -- If we called open_preview during an autocmd, then the edit command may not trigger the - -- BufReadCmd to load the buffer. So we need to do it manually. - if util.is_oil_bufnr(filebufnr) then - M.load_oil_buffer(filebufnr) - end - - vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 }) - vim.api.nvim_win_set_var(0, "oil_preview", true) - for k, v in pairs(config.preview_win.win_options) do - vim.api.nvim_set_option_value(k, v, { scope = "local", win = preview_win }) - end - vim.w.oil_entry_id = entry.id - vim.w.oil_source_win = prev_win - if has_multicursors then - hack_set_win(prev_win) - mc.restoreCursors() - elseif is_visual_mode then - hack_set_win(prev_win) - -- Restore the visual selection - vim.cmd.normal({ args = { "gv" }, bang = true }) - else - vim.api.nvim_set_current_win(prev_win) - end - finish() - end) -end - ----@class (exact) oil.SelectOpts ----@field vertical? boolean Open the buffer in a vertical split ----@field horizontal? boolean Open the buffer in a horizontal split ----@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier ----@field tab? boolean Open the buffer in a new tab ----@field close? boolean Close the original oil buffer once selection is made ----@field handle_buffer_callback? fun(buf_id: integer) If defined, all other buffer related options here would be ignored. This callback allows you to take over the process of opening the buffer yourself. - ---Select the entry under the cursor ----@param opts nil|oil.SelectOpts +---@param opts nil|table +--- vertical boolean Open the buffer in a vertical split +--- horizontal boolean Open the buffer in a horizontal split +--- split "aboveleft"|"belowright"|"topleft"|"botright" Split modifier +--- preview boolean Open the buffer in a preview window +--- tab boolean Open the buffer in a new tab +--- close boolean Close the original oil buffer once selection is made ---@param callback nil|fun(err: nil|string) Called once all entries have been opened M.select = function(opts, callback) local cache = require("oil.cache") local config = require("oil.config") - local constants = require("oil.constants") - local util = require("oil.util") - local FIELD_META = constants.FIELD_META opts = vim.tbl_extend("keep", opts or {}, {}) - local function finish(err) if err then vim.notify(err, vim.log.levels.ERROR) @@ -649,27 +418,40 @@ M.select = function(opts, callback) callback(err) end end - if not opts.split and (opts.horizontal or opts.vertical) then - if opts.horizontal then - opts.split = vim.o.splitbelow and "belowright" or "aboveleft" - else - opts.split = vim.o.splitright and "belowright" or "aboveleft" - end + if opts.horizontal or opts.vertical or opts.preview then + opts.split = opts.split or "belowright" end - if opts.tab and opts.split then - return finish("Cannot use split=true when tab = true") + if opts.preview and not opts.horizontal and opts.vertical == nil then + opts.vertical = true + end + if opts.tab and (opts.preview or opts.split) then + return finish("Cannot set preview or split when tab = true") + end + if opts.close and opts.preview then + return finish("Cannot use close=true with preview=true") + end + local util = require("oil.util") + if util.is_floating_win() and opts.preview then + return finish("oil preview doesn't work in a floating window") end local adapter = util.get_adapter(0) if not adapter then - return finish("Not an oil buffer") + return finish("Could not find adapter for current buffer") end - local visual_range = util.get_visual_range() + local mode = vim.api.nvim_get_mode().mode + local is_visual = mode:match("^[vV]") - ---@type oil.Entry[] local entries = {} - if visual_range then - for i = visual_range.start_lnum, visual_range.end_lnum do + if is_visual then + -- This is the best way to get the visual selection at the moment + -- https://github.com/neovim/neovim/pull/13896 + local _, start_lnum, _, _ = unpack(vim.fn.getpos("v")) + local _, end_lnum, _, _, _ = unpack(vim.fn.getcurpos()) + if start_lnum > end_lnum then + start_lnum, end_lnum = end_lnum, start_lnum + end + for i = start_lnum, end_lnum do local entry = M.get_entry_on_line(0, i) if entry then table.insert(entries, entry) @@ -684,30 +466,24 @@ M.select = function(opts, callback) if vim.tbl_isempty(entries) then return finish("Could not find entry under cursor") end + if #entries > 1 and opts.preview then + vim.notify("Cannot preview multiple entries", vim.log.levels.WARN) + entries = { entries[1] } + end -- Check if any of these entries are moved from their original location local bufname = vim.api.nvim_buf_get_name(0) local any_moved = false for _, entry in ipairs(entries) do - -- Ignore entries with ID 0 (typically the "../" entry) - if entry.id ~= 0 then - local is_new_entry = entry.id == nil - local is_moved_from_dir = entry.id and cache.get_parent_url(entry.id) ~= bufname - local is_renamed = entry.parsed_name ~= entry.name - local internal_entry = entry.id and cache.get_entry_by_id(entry.id) - if internal_entry then - local meta = internal_entry[FIELD_META] - if meta and meta.display_name then - is_renamed = entry.parsed_name ~= meta.display_name - end - end - if is_new_entry or is_moved_from_dir or is_renamed then - any_moved = true - break - end + local is_new_entry = entry.id == nil + local is_moved_from_dir = entry.id and cache.get_parent_url(entry.id) ~= bufname + local is_renamed = entry.parsed_name ~= entry.name + if is_new_entry or is_moved_from_dir or is_renamed then + any_moved = true + break end end - if any_moved and config.prompt_save_on_select_new_entry then + if any_moved and not opts.preview and config.prompt_save_on_select_new_entry then local ok, choice = pcall(vim.fn.confirm, "Save changes?", "Yes\nNo", 1) if not ok then return finish() @@ -717,8 +493,12 @@ M.select = function(opts, callback) end end + -- Close the preview window if we're not previewing the selection + local preview_win = util.get_preview_win() + if not opts.preview and preview_win then + vim.api.nvim_win_close(preview_win, true) + end local prev_win = vim.api.nvim_get_current_win() - local oil_bufnr = vim.api.nvim_get_current_buf() -- Async iter over entries so we can normalize the url before opening local i = 1 @@ -728,7 +508,18 @@ M.select = function(opts, callback) if not entry then return cb() end - if util.is_directory(entry) then + local scheme, dir = util.parse_url(bufname) + local child = dir .. entry.name + local url = scheme .. child + local is_directory = entry.type == "directory" + or ( + entry.type == "link" + and entry.meta + and entry.meta.link_stat + and entry.meta.link_stat.type == "directory" + ) + if is_directory then + url = url .. "/" -- If this is a new directory BUT we think we already have an entry with this name, disallow -- entry. This prevents the case of MOVE /foo -> /bar + CREATE /foo. -- If you enter the new /foo, it will show the contents of the old /foo. @@ -736,53 +527,58 @@ M.select = function(opts, callback) return cb("Please save changes before entering new directory") end else - -- Close floating window before opening a file if vim.w.is_oil_win then - M.close() + vim.api.nvim_win_close(0, false) end end -- Normalize the url before opening to prevent needing to rename them inside the BufReadCmd -- Renaming buffers during opening can lead to missed autocmds - util.get_edit_path(oil_bufnr, entry, function(normalized_url) + adapter.normalize_url(url, function(normalized_url) local mods = { vertical = opts.vertical, horizontal = opts.horizontal, split = opts.split, - keepalt = false, + keepalt = true, } - local filebufnr = vim.fn.bufadd(normalized_url) - local entry_is_file = not vim.endswith(normalized_url, "/") + local filename = util.escape_filename(normalized_url) - -- The :buffer command doesn't set buflisted=true - -- So do that for normal files or for oil dirs if config set buflisted=true - if entry_is_file or config.buf_options.buflisted then - vim.bo[filebufnr].buflisted = true - end - - local cmd = "buffer" - if opts.tab then - vim.cmd.tabnew({ mods = mods }) - -- Make sure the new buffer from tabnew gets cleaned up - vim.bo.bufhidden = "wipe" - elseif opts.split then - cmd = "sbuffer" - end - if opts.handle_buffer_callback ~= nil then - opts.handle_buffer_callback(filebufnr) - else - ---@diagnostic disable-next-line: param-type-mismatch - local ok, err = pcall(vim.cmd, { - cmd = cmd, - args = { filebufnr }, - mods = mods, - }) - -- Ignore swapfile errors - if not ok and err and not err:match("^Vim:E325:") then - vim.api.nvim_echo({ { err, "Error" } }, true, {}) + -- If we're previewing a file that hasn't been opened yet, make sure it gets deleted after we + -- close the window + if opts.preview and not util.parse_url(filename) then + local bufnr = vim.fn.bufadd(filename) + if vim.fn.bufloaded(bufnr) == 0 then + vim.bo[bufnr].bufhidden = "wipe" + vim.b[bufnr].oil_preview_buffer = true end end + local cmd + if opts.preview and preview_win then + vim.api.nvim_set_current_win(preview_win) + cmd = "edit" + else + if vim.tbl_isempty(mods) then + mods = nil + end + if opts.tab then + cmd = "tabedit" + elseif opts.split then + cmd = "split" + else + cmd = "edit" + end + end + vim.cmd({ + cmd = cmd, + args = { filename }, + mods = mods, + }) + if opts.preview then + vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 }) + vim.w.oil_entry_id = entry.id + vim.api.nvim_set_current_win(prev_win) + end open_next_entry(cb) end) end @@ -800,9 +596,6 @@ M.select = function(opts, callback) M.close() end) end - - update_preview_window() - finish() end) end @@ -811,7 +604,6 @@ end ---@return boolean local function maybe_hijack_directory_buffer(bufnr) local config = require("oil.config") - local fs = require("oil.fs") local util = require("oil.util") if not config.default_file_explorer then return false @@ -823,35 +615,20 @@ local function maybe_hijack_directory_buffer(bufnr) if util.parse_url(bufname) or vim.fn.isdirectory(bufname) == 0 then return false end - local new_name = util.addslash( - config.adapter_to_scheme.files .. fs.os_to_posix_path(vim.fn.fnamemodify(bufname, ":p")) + local replaced = util.rename_buffer( + bufnr, + util.addslash(config.adapter_to_scheme.files .. vim.fn.fnamemodify(bufname, ":p")) ) - local replaced = util.rename_buffer(bufnr, new_name) return not replaced end ---@private M._get_highlights = function() return { - { - name = "OilEmpty", - link = "Comment", - desc = "Empty column values", - }, - { - name = "OilHidden", - link = "Comment", - desc = "Hidden entry in an oil buffer", - }, { name = "OilDir", link = "Directory", - desc = "Directory names in an oil buffer", - }, - { - name = "OilDirHidden", - link = "OilHidden", - desc = "Hidden directory names in an oil buffer", + desc = "Directories in an oil buffer", }, { name = "OilDirIcon", @@ -863,61 +640,16 @@ M._get_highlights = function() link = "Keyword", desc = "Socket files in an oil buffer", }, - { - name = "OilSocketHidden", - link = "OilHidden", - desc = "Hidden socket files in an oil buffer", - }, { name = "OilLink", link = nil, desc = "Soft links in an oil buffer", }, - { - name = "OilOrphanLink", - link = nil, - desc = "Orphaned soft links in an oil buffer", - }, - { - name = "OilLinkHidden", - link = "OilHidden", - desc = "Hidden soft links in an oil buffer", - }, - { - name = "OilOrphanLinkHidden", - link = "OilLinkHidden", - desc = "Hidden orphaned soft links in an oil buffer", - }, - { - name = "OilLinkTarget", - link = "Comment", - desc = "The target of a soft link", - }, - { - name = "OilOrphanLinkTarget", - link = "DiagnosticError", - desc = "The target of an orphaned soft link", - }, - { - name = "OilLinkTargetHidden", - link = "OilHidden", - desc = "The target of a hidden soft link", - }, - { - name = "OilOrphanLinkTargetHidden", - link = "OilOrphanLinkTarget", - desc = "The target of an hidden orphaned soft link", - }, { name = "OilFile", link = nil, desc = "Normal files in an oil buffer", }, - { - name = "OilFileHidden", - link = "OilHidden", - desc = "Hidden normal files in an oil buffer", - }, { name = "OilCreate", link = "DiagnosticInfo", @@ -943,26 +675,6 @@ M._get_highlights = function() link = "Special", desc = "Change action in the oil preview window", }, - { - name = "OilRestore", - link = "OilCreate", - desc = "Restore (from the trash) action in the oil preview window", - }, - { - name = "OilPurge", - link = "OilDelete", - desc = "Purge (Permanently delete a file from trash) action in the oil preview window", - }, - { - name = "OilTrash", - link = "OilDelete", - desc = "Trash (delete a file to trash) action in the oil preview window", - }, - { - name = "OilTrashSourcePath", - link = "Comment", - desc = "Virtual text that shows the original path of file in the trash", - }, } end @@ -972,13 +684,8 @@ local function set_colors() vim.api.nvim_set_hl(0, conf.name, { default = true, link = conf.link }) end end - -- TODO can remove this call once we drop support for Neovim 0.8. FloatTitle was introduced as a - -- built-in highlight group in 0.9, and we can start to rely on colorschemes setting it. - ---@diagnostic disable-next-line: deprecated - if vim.fn.has("nvim-0.9") == 0 and not pcall(vim.api.nvim_get_hl_by_name, "FloatTitle", true) then - ---@diagnostic disable-next-line: deprecated + if not pcall(vim.api.nvim_get_hl_by_name, "FloatTitle") then local border = vim.api.nvim_get_hl_by_name("FloatBorder", true) - ---@diagnostic disable-next-line: deprecated local normal = vim.api.nvim_get_hl_by_name("Normal", true) vim.api.nvim_set_hl( 0, @@ -991,23 +698,14 @@ end ---Save all changes ---@param opts nil|table --- confirm nil|boolean Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil ----@param cb? fun(err: nil|string) Called when mutations complete. ----@note ---- If you provide your own callback function, there will be no notification for errors. -M.save = function(opts, cb) +M.save = function(opts) opts = opts or {} - if not cb then - cb = function(err) - if err and err ~= "Canceled" then - vim.notify(err, vim.log.levels.ERROR) - end - end - end local mutator = require("oil.mutator") - mutator.try_write_changes(opts.confirm, cb) + mutator.try_write_changes(opts.confirm) end local function restore_alt_buf() + local config = require("oil.config") if vim.bo.filetype == "oil" then require("oil.view").set_win_options() vim.api.nvim_win_set_var(0, "oil_did_enter", true) @@ -1030,12 +728,15 @@ local function restore_alt_buf() end end end + + if config.restore_win_options then + require("oil.view").restore_win_options() + end end end ----@private ---@param bufnr integer -M.load_oil_buffer = function(bufnr) +local function load_oil_buffer(bufnr) local config = require("oil.config") local keymap_util = require("oil.keymap_util") local loading = require("oil.loading") @@ -1049,12 +750,7 @@ M.load_oil_buffer = function(bufnr) util.rename_buffer(bufnr, bufname) end - -- Early return if we're already loading or have already loaded this buffer - if loading.is_loading(bufnr) or vim.b[bufnr].filetype ~= nil then - return - end - - local adapter = assert(config.get_adapter_by_scheme(scheme)) + local adapter = config.get_adapter_by_scheme(scheme) if vim.endswith(bufname, "/") then -- This is a small quality-of-life thing. If the buffer name ends with a `/`, we know it's a @@ -1062,7 +758,7 @@ M.load_oil_buffer = function(bufnr) -- (e.g. ssh) because it will set up the filetype keybinds at the *beginning* of the loading -- process. vim.bo[bufnr].filetype = "oil" - keymap_util.set_keymaps(config.keymaps, bufnr) + keymap_util.set_keymaps("", config.keymaps, bufnr) end loading.set_loading(bufnr, true) local winid = vim.api.nvim_get_current_win() @@ -1110,106 +806,40 @@ M.load_oil_buffer = function(bufnr) adapter.normalize_url(bufname, finish) end -local function close_preview_window_if_not_in_oil() - local util = require("oil.util") - local preview_win_id = util.get_preview_win() - if not preview_win_id or not vim.w[preview_win_id].oil_entry_id then - return - end - - local oil_source_win = vim.w[preview_win_id].oil_source_win - if oil_source_win and vim.api.nvim_win_is_valid(oil_source_win) then - local src_buf = vim.api.nvim_win_get_buf(oil_source_win) - if util.is_oil_bufnr(src_buf) then - return - end - end - - -- This can fail if it's the last window open - pcall(vim.api.nvim_win_close, preview_win_id, true) -end - -local _on_key_ns = 0 ---Initialize oil ----@param opts oil.setupOpts|nil +---@param opts nil|table M.setup = function(opts) - local Ringbuf = require("oil.ringbuf") local config = require("oil.config") config.setup(opts) set_colors() - local callback = function(args) - local util = require("oil.util") - if args.smods.tab > 0 then + vim.api.nvim_create_user_command("Oil", function(args) + if args.smods.tab == 1 then vim.cmd.tabnew() end local float = false - local trash = false - local preview = false - local i = 1 - while i <= #args.fargs do - local v = args.fargs[i] + for i, v in ipairs(args.fargs) do if v == "--float" then float = true table.remove(args.fargs, i) - elseif v == "--trash" then - trash = true - table.remove(args.fargs, i) - elseif v == "--preview" then - -- In the future we may want to support specifying options for the preview window (e.g. - -- vertical/horizontal), but if you want that level of control maybe just use the API - preview = true - table.remove(args.fargs, i) - elseif v == "--progress" then - local mutator = require("oil.mutator") - if mutator.is_mutating() then - mutator.show_progress() - else - vim.notify("No mutation in progress", vim.log.levels.WARN) - end - return - else - i = i + 1 end end - if not float and (args.smods.vertical or args.smods.horizontal or args.smods.split ~= "") then - local range = args.count > 0 and { args.count } or nil - local cmdargs = { mods = { split = args.smods.split }, range = range } + if not float and (args.smods.vertical or args.smods.split ~= "") then if args.smods.vertical then - vim.cmd.vsplit(cmdargs) + vim.cmd.vsplit({ mods = { split = args.smods.split } }) else - vim.cmd.split(cmdargs) + vim.cmd.split({ mods = { split = args.smods.split } }) end end local method = float and "open_float" or "open" - local path = args.fargs[1] - local open_opts = {} - if trash then - local url = M.get_url_for_path(path, false) - local _, new_path = util.parse_url(url) - path = "oil-trash://" .. new_path - end - if preview then - open_opts.preview = {} - end - M[method](path, open_opts) - end - vim.api.nvim_create_user_command( - "Oil", - callback, - { desc = "Open oil file browser on a directory", nargs = "*", complete = "dir", count = true } - ) + M[method](unpack(args.fargs)) + end, { desc = "Open oil file browser on a directory", nargs = "*", complete = "dir" }) local aug = vim.api.nvim_create_augroup("Oil", {}) - if config.default_file_explorer then - vim.g.loaded_netrw = 1 - vim.g.loaded_netrwPlugin = 1 - -- If netrw was already loaded, clear this augroup - if vim.fn.exists("#FileExplorer") then - vim.api.nvim_create_augroup("FileExplorer", { clear = true }) - end + if config.default_file_explorer and vim.fn.exists("#FileExplorer") then + vim.api.nvim_create_augroup("FileExplorer", { clear = true }) end local patterns = {} @@ -1229,12 +859,6 @@ M.setup = function(opts) pattern = filetype_patterns, }) - local keybuf = Ringbuf.new(7) - if _on_key_ns == 0 then - _on_key_ns = vim.on_key(function(char) - keybuf:push(char) - end, _on_key_ns) - end vim.api.nvim_create_autocmd("ColorScheme", { desc = "Set default oil highlights", group = aug, @@ -1246,7 +870,7 @@ M.setup = function(opts) pattern = scheme_pattern, nested = true, callback = function(params) - M.load_oil_buffer(params.buf) + load_oil_buffer(params.buf) end, }) vim.api.nvim_create_autocmd("BufWriteCmd", { @@ -1254,34 +878,13 @@ M.setup = function(opts) pattern = scheme_pattern, nested = true, callback = function(params) - local last_keys = keybuf:as_str() - local winid = vim.api.nvim_get_current_win() - -- If the user issued a :wq or similar, we should quit after saving - local quit_after_save = vim.endswith(last_keys, ":wq\r") - or vim.endswith(last_keys, ":x\r") - or vim.endswith(last_keys, "ZZ") - local quit_all = vim.endswith(last_keys, ":wqa\r") - or vim.endswith(last_keys, ":wqal\r") - or vim.endswith(last_keys, ":wqall\r") local bufname = vim.api.nvim_buf_get_name(params.buf) if vim.endswith(bufname, "/") then vim.cmd.doautocmd({ args = { "BufWritePre", params.file }, mods = { silent = true } }) - M.save(nil, function(err) - if err then - if err ~= "Canceled" then - vim.notify(err, vim.log.levels.ERROR) - end - elseif winid == vim.api.nvim_get_current_win() then - if quit_after_save then - vim.cmd.quit() - elseif quit_all then - vim.cmd.quitall() - end - end - end) + M.save() vim.cmd.doautocmd({ args = { "BufWritePost", params.file }, mods = { silent = true } }) else - local adapter = assert(config.get_adapter_by_scheme(bufname)) + local adapter = config.get_adapter_by_scheme(bufname) adapter.write_file(params.buf) end end, @@ -1295,7 +898,6 @@ M.setup = function(opts) if not util.is_oil_bufnr(0) then vim.w.oil_original_buffer = vim.api.nvim_get_current_buf() vim.w.oil_original_view = vim.fn.winsaveview() - ---@diagnostic disable-next-line: param-type-mismatch vim.w.oil_original_alternate = vim.fn.bufnr("#") end end, @@ -1308,28 +910,24 @@ M.setup = function(opts) local util = require("oil.util") local bufname = vim.api.nvim_buf_get_name(0) local scheme = util.parse_url(bufname) - local is_oil_buf = scheme and config.adapters[scheme] - -- We want to filter out oil buffers that are not directories (i.e. ssh files) - local is_oil_dir_or_unknown = (vim.bo.filetype == "oil" or vim.bo.filetype == "") - if is_oil_buf and is_oil_dir_or_unknown then - local view = require("oil.view") - view.maybe_set_cursor() + if scheme and config.adapters[scheme] then + require("oil.view").maybe_set_cursor() -- While we are in an oil buffer, set the alternate file to the buffer we were in prior to -- opening oil local has_orig, orig_buffer = pcall(vim.api.nvim_win_get_var, 0, "oil_original_buffer") if has_orig and vim.api.nvim_buf_is_valid(orig_buffer) then vim.fn.setreg("#", orig_buffer) end - view.set_win_options() - vim.w.oil_did_enter = true + if not vim.w.oil_did_enter then + require("oil.view").set_win_options() + vim.w.oil_did_enter = true + end elseif vim.fn.isdirectory(bufname) == 0 then -- Only run this logic if we are *not* in an oil buffer (and it's not a directory, which -- will be replaced by an oil:// url) -- Oil buffers have to run it in BufReadCmd after confirming they are a directory or a file restore_alt_buf() end - - close_preview_window_if_not_in_oil() end, }) @@ -1340,7 +938,7 @@ M.setup = function(opts) callback = function() -- If we have entered a "preview" buffer in a non-preview window, reset bufhidden if vim.b.oil_preview_buffer and not vim.wo.previewwindow then - vim.bo.bufhidden = vim.api.nvim_get_option_value("bufhidden", { scope = "global" }) + vim.bo.bufhidden = vim.o.bufhidden vim.b.oil_preview_buffer = nil end end, @@ -1359,6 +957,24 @@ M.setup = function(opts) end, }) end + if + vim.g.loaded_netrwPlugin ~= 1 + and not config.silence_netrw_warning + and config.default_file_explorer + then + vim.api.nvim_create_autocmd("FileType", { + desc = "Inform user how to disable netrw", + group = aug, + pattern = "netrw", + once = true, + callback = function() + vim.notify( + "If you expected an Oil buffer here, you may want to disable netrw (:help netrw-noload)\nSet `default_file_explorer = false` in oil.setup() to not take over netrw buffers, or `silence_netrw_warning = true` to disable this message.", + vim.log.levels.WARN + ) + end, + }) + end vim.api.nvim_create_autocmd("WinNew", { desc = "Restore window options when splitting an oil window", group = aug, @@ -1395,6 +1011,13 @@ M.setup = function(opts) vim.w.oil_original_buffer = vim.w[parent_win].oil_original_buffer vim.w.oil_original_view = vim.w[parent_win].oil_original_view vim.w.oil_original_alternate = vim.w[parent_win].oil_original_alternate + for k in pairs(config.win_options) do + local varname = "_oil_" .. k + local has_opt, opt = pcall(vim.api.nvim_win_get_var, parent_win, varname) + if has_opt then + vim.api.nvim_win_set_var(0, varname, opt) + end + end end, }) vim.api.nvim_create_autocmd("BufAdd", { @@ -1413,13 +1036,13 @@ M.setup = function(opts) group = aug, pattern = "*", callback = function(params) - if vim.g.SessionLoad ~= 1 then - return - end local util = require("oil.util") - local scheme = util.parse_url(params.file) - if config.adapters[scheme] and vim.api.nvim_buf_line_count(params.buf) == 1 then - M.load_oil_buffer(params.buf) + for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do + local bufname = vim.api.nvim_buf_get_name(bufnr) + local scheme = util.parse_url(bufname) + if config.adapters[scheme] and vim.api.nvim_buf_line_count(bufnr) == 1 then + load_oil_buffer(bufnr) + end end end, }) @@ -1428,7 +1051,7 @@ M.setup = function(opts) if maybe_hijack_directory_buffer(bufnr) and vim.v.vim_did_enter == 1 then -- manually call load on a hijacked directory buffer if vim has already entered -- (the BufReadCmd will not trigger) - M.load_oil_buffer(bufnr) + load_oil_buffer(bufnr) end end diff --git a/lua/oil/keymap_util.lua b/lua/oil/keymap_util.lua index 8c58738..3a582fd 100644 --- a/lua/oil/keymap_util.lua +++ b/lua/oil/keymap_util.lua @@ -1,79 +1,28 @@ local actions = require("oil.actions") -local config = require("oil.config") local layout = require("oil.layout") local util = require("oil.util") local M = {} ----@param rhs string|table|fun() ----@return string|fun() rhs ----@return table opts ----@return string|nil mode local function resolve(rhs) if type(rhs) == "string" and vim.startswith(rhs, "actions.") then - local action_name = vim.split(rhs, ".", { plain = true })[2] - local action = actions[action_name] - if not action then - vim.notify("[oil.nvim] Unknown action name: " .. action_name, vim.log.levels.ERROR) - end - return resolve(action) + return resolve(actions[vim.split(rhs, ".", true)[2]]) elseif type(rhs) == "table" then local opts = vim.deepcopy(rhs) - -- We support passing in a `callback` key, or using the 1 index as the rhs of the keymap - local callback, parent_opts = resolve(opts.callback or opts[1]) - - -- Fall back to the parent desc, adding the opts as a string if it exists - if parent_opts.desc and not opts.desc then - if opts.opts then - opts.desc = - string.format("%s %s", parent_opts.desc, vim.inspect(opts.opts):gsub("%s+", " ")) - else - opts.desc = parent_opts.desc - end - end - - local mode = opts.mode - if type(rhs.callback) == "string" then - local action_opts, action_mode - callback, action_opts, action_mode = resolve(rhs.callback) - opts = vim.tbl_extend("keep", opts, action_opts) - mode = mode or action_mode - end - - -- remove all the keys that we can't pass as options to `vim.keymap.set` opts.callback = nil - opts.mode = nil - opts[1] = nil - opts.deprecated = nil - opts.parameters = nil - - if opts.opts and type(callback) == "function" then - local callback_args = opts.opts - opts.opts = nil - local orig_callback = callback - callback = function() - ---@diagnostic disable-next-line: redundant-parameter - orig_callback(callback_args) - end - end - - return callback, opts, mode - else - return rhs, {} + return rhs.callback, opts end + return rhs, {} end ----@param keymaps table ----@param bufnr integer -M.set_keymaps = function(keymaps, bufnr) +M.set_keymaps = function(mode, keymaps, bufnr) for k, v in pairs(keymaps) do - local rhs, opts, mode = resolve(v) + local rhs, opts = resolve(v) if rhs then - vim.keymap.set(mode or "", k, rhs, vim.tbl_extend("keep", { buffer = bufnr }, opts)) + vim.keymap.set(mode, k, rhs, vim.tbl_extend("keep", { buffer = bufnr }, opts)) end end end ----@param keymaps table M.show_help = function(keymaps) local rhs_to_lhs = {} local lhs_to_all_lhs = {} @@ -89,30 +38,31 @@ M.show_help = function(keymaps) end end + local col_left = {} + local col_desc = {} local max_lhs = 1 - local keymap_entries = {} for k, rhs in pairs(keymaps) do local all_lhs = lhs_to_all_lhs[k] if all_lhs then local _, opts = resolve(rhs) local keystr = table.concat(all_lhs, "/") max_lhs = math.max(max_lhs, vim.api.nvim_strwidth(keystr)) - table.insert(keymap_entries, { str = keystr, all_lhs = all_lhs, desc = opts.desc or "" }) + table.insert(col_left, { str = keystr, all_lhs = all_lhs }) + table.insert(col_desc, opts.desc or "") end end - table.sort(keymap_entries, function(a, b) - return a.desc < b.desc - end) local lines = {} local highlights = {} local max_line = 1 - for _, entry in ipairs(keymap_entries) do - local line = string.format(" %s %s", util.pad_align(entry.str, max_lhs, "left"), entry.desc) + for i = 1, #col_left do + local left = col_left[i] + local desc = col_desc[i] + local line = string.format(" %s %s", util.rpad(left.str, max_lhs), desc) max_line = math.max(max_line, vim.api.nvim_strwidth(line)) table.insert(lines, line) local start = 1 - for _, key in ipairs(entry.all_lhs) do + for _, key in ipairs(left.all_lhs) do local keywidth = vim.api.nvim_strwidth(key) table.insert(highlights, { "Special", #lines, start, start + keywidth }) start = start + keywidth + 1 @@ -131,8 +81,8 @@ M.show_help = function(keymaps) end vim.keymap.set("n", "q", "close", { buffer = bufnr }) vim.keymap.set("n", "", "close", { buffer = bufnr }) - vim.bo[bufnr].modifiable = false - vim.bo[bufnr].bufhidden = "wipe" + vim.api.nvim_buf_set_option(bufnr, "modifiable", false) + vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe") local editor_width = vim.o.columns local editor_height = layout.get_editor_height() @@ -144,7 +94,7 @@ M.show_help = function(keymaps) height = math.min(editor_height, #lines), zindex = 150, style = "minimal", - border = config.keymaps_help.border, + border = "rounded", }) local function close() if vim.api.nvim_win_is_valid(winid) then diff --git a/lua/oil/layout.lua b/lua/oil/layout.lua index 6d4563d..b4e3fed 100644 --- a/lua/oil/layout.lua +++ b/lua/oil/layout.lua @@ -1,15 +1,10 @@ local M = {} ----@param value number ----@return boolean local function is_float(value) local _, p = math.modf(value) return p ~= 0 end ----@param value number ----@param max_value number ----@return number local function calc_float(value, max_value) if value and is_float(value) then return math.min(max_value, value * max_value) @@ -98,97 +93,6 @@ M.calculate_height = function(desired_height, opts) ) end ----@class (exact) oil.WinLayout ----@field width integer ----@field height integer ----@field row integer ----@field col integer - ----@return vim.api.keyset.win_config -M.get_fullscreen_win_opts = function() - local config = require("oil.config") - - local total_width = M.get_editor_width() - local total_height = M.get_editor_height() - local width = total_width - 2 * config.float.padding - if config.float.border ~= "none" then - width = width - 2 -- The border consumes 1 col on each side - end - if config.float.max_width > 0 then - local max_width = math.floor(calc_float(config.float.max_width, total_width)) - width = math.min(width, max_width) - end - local height = total_height - 2 * config.float.padding - if config.float.max_height > 0 then - local max_height = math.floor(calc_float(config.float.max_height, total_height)) - height = math.min(height, max_height) - end - local row = math.floor((total_height - height) / 2) - local col = math.floor((total_width - width) / 2) - 1 -- adjust for border width - - local win_opts = { - relative = "editor", - width = width, - height = height, - row = row, - col = col, - border = config.float.border, - zindex = 45, - } - return config.float.override(win_opts) or win_opts -end - ----@param winid integer ----@param direction "above"|"below"|"left"|"right"|"auto" ----@param gap integer ----@return oil.WinLayout root_dim New dimensions of the original window ----@return oil.WinLayout new_dim New dimensions of the new window -M.split_window = function(winid, direction, gap) - if direction == "auto" then - direction = vim.o.splitright and "right" or "left" - end - - local float_config = vim.api.nvim_win_get_config(winid) - ---@type oil.WinLayout - local dim_root = { - width = float_config.width, - height = float_config.height, - col = float_config.col, - row = float_config.row, - } - if vim.fn.has("nvim-0.10") == 0 then - -- read https://github.com/neovim/neovim/issues/24430 for more infos. - dim_root.col = float_config.col[vim.val_idx] - dim_root.row = float_config.row[vim.val_idx] - end - local dim_new = vim.deepcopy(dim_root) - - if direction == "left" or direction == "right" then - dim_new.width = math.floor(float_config.width / 2) - math.ceil(gap / 2) - dim_root.width = dim_new.width - else - dim_new.height = math.floor(float_config.height / 2) - math.ceil(gap / 2) - dim_root.height = dim_new.height - end - - if direction == "left" then - dim_root.col = dim_root.col + dim_root.width + gap - elseif direction == "right" then - dim_new.col = dim_new.col + dim_new.width + gap - elseif direction == "above" then - dim_root.row = dim_root.row + dim_root.height + gap - elseif direction == "below" then - dim_new.row = dim_new.row + dim_new.height + gap - end - - return dim_root, dim_new -end - ----@param desired_width integer ----@param desired_height integer ----@param opts table ----@return integer width ----@return integer height M.calculate_dims = function(desired_width, desired_height, opts) local width = M.calculate_width(desired_width, opts) local height = M.calculate_height(desired_height, opts) diff --git a/lua/oil/loading.lua b/lua/oil/loading.lua index 6e575c5..9ec6006 100644 --- a/lua/oil/loading.lua +++ b/lua/oil/loading.lua @@ -16,7 +16,7 @@ local spinners = { } ---@param name_or_frames string|string[] ----@return fun(): string +---@return fun() M.get_iter = function(name_or_frames) local frames if type(name_or_frames) == "string" then @@ -67,15 +67,14 @@ M.set_loading = function(bufnr, is_loading) timers[bufnr] = vim.loop.new_timer() local bar_iter = M.get_bar_iter({ width = width }) timers[bufnr]:start( - 200, -- Delay the loading screen just a bit to avoid flicker + 100, -- Delay the loading screen just a bit to avoid flicker math.floor(1000 / FPS), vim.schedule_wrap(function() if not vim.api.nvim_buf_is_valid(bufnr) or not timers[bufnr] then M.set_loading(bufnr, false) return end - local lines = - { util.pad_align("Loading", math.floor(width / 2) - 3, "right"), bar_iter() } + local lines = { util.lpad("Loading", math.floor(width / 2) - 3), bar_iter() } util.render_text(bufnr, lines) end) ) diff --git a/lua/oil/log.lua b/lua/oil/log.lua deleted file mode 100644 index 28a4f9c..0000000 --- a/lua/oil/log.lua +++ /dev/null @@ -1,126 +0,0 @@ -local uv = vim.uv or vim.loop -local levels_reverse = {} -for k, v in pairs(vim.log.levels) do - levels_reverse[v] = k -end - -local Log = {} - ----@type integer -Log.level = vim.log.levels.WARN - ----@return string -Log.get_logfile = function() - local fs = require("oil.fs") - - local ok, stdpath = pcall(vim.fn.stdpath, "log") - if not ok then - stdpath = vim.fn.stdpath("cache") - end - assert(type(stdpath) == "string") - return fs.join(stdpath, "oil.log") -end - ----@param level integer ----@param msg string ----@param ... any[] ----@return string -local function format(level, msg, ...) - local args = vim.F.pack_len(...) - for i = 1, args.n do - local v = args[i] - if type(v) == "table" then - args[i] = vim.inspect(v) - elseif v == nil then - args[i] = "nil" - end - end - local ok, text = pcall(string.format, msg, vim.F.unpack_len(args)) - -- TODO figure out how to get formatted time inside luv callback - -- local timestr = vim.fn.strftime("%Y-%m-%d %H:%M:%S") - local timestr = "" - if ok then - local str_level = levels_reverse[level] - return string.format("%s[%s] %s", timestr, str_level, text) - else - return string.format( - "%s[ERROR] error formatting log line: '%s' args %s", - timestr, - vim.inspect(msg), - vim.inspect(args) - ) - end -end - ----@param line string -local function write(line) - -- This will be replaced during initialization -end - -local initialized = false -local function initialize() - if initialized then - return - end - initialized = true - local filepath = Log.get_logfile() - - local stat = uv.fs_stat(filepath) - if stat and stat.size > 10 * 1024 * 1024 then - local backup = filepath .. ".1" - uv.fs_unlink(backup) - uv.fs_rename(filepath, backup) - end - - local parent = vim.fs.dirname(filepath) - require("oil.fs").mkdirp(parent) - - local logfile, openerr = io.open(filepath, "a+") - if not logfile then - local err_msg = string.format("Failed to open oil.nvim log file: %s", openerr) - vim.notify(err_msg, vim.log.levels.ERROR) - else - write = function(line) - logfile:write(line) - logfile:write("\n") - logfile:flush() - end - end -end - ----Override the file handler e.g. for tests ----@param handler fun(line: string) -function Log.set_handler(handler) - write = handler - initialized = true -end - -function Log.log(level, msg, ...) - if Log.level <= level then - initialize() - local text = format(level, msg, ...) - write(text) - end -end - -function Log.trace(...) - Log.log(vim.log.levels.TRACE, ...) -end - -function Log.debug(...) - Log.log(vim.log.levels.DEBUG, ...) -end - -function Log.info(...) - Log.log(vim.log.levels.INFO, ...) -end - -function Log.warn(...) - Log.log(vim.log.levels.WARN, ...) -end - -function Log.error(...) - Log.log(vim.log.levels.ERROR, ...) -end - -return Log diff --git a/lua/oil/lsp/helpers.lua b/lua/oil/lsp/helpers.lua deleted file mode 100644 index 45264dc..0000000 --- a/lua/oil/lsp/helpers.lua +++ /dev/null @@ -1,121 +0,0 @@ -local config = require("oil.config") -local fs = require("oil.fs") -local util = require("oil.util") -local workspace = require("oil.lsp.workspace") - -local M = {} - ----@param actions oil.Action[] ----@return fun() did_perform Call this function when the file operations have been completed -M.will_perform_file_operations = function(actions) - local moves = {} - local creates = {} - local deletes = {} - for _, action in ipairs(actions) do - if action.type == "move" then - local src_scheme, src_path = util.parse_url(action.src_url) - assert(src_path) - local src_adapter = assert(config.get_adapter_by_scheme(src_scheme)) - local dest_scheme, dest_path = util.parse_url(action.dest_url) - local dest_adapter = assert(config.get_adapter_by_scheme(dest_scheme)) - src_path = fs.posix_to_os_path(src_path) - dest_path = fs.posix_to_os_path(assert(dest_path)) - if src_adapter.name == "files" and dest_adapter.name == "files" then - moves[src_path] = dest_path - elseif src_adapter.name == "files" then - table.insert(deletes, src_path) - elseif dest_adapter.name == "files" then - table.insert(creates, src_path) - end - elseif action.type == "create" then - local scheme, path = util.parse_url(action.url) - path = fs.posix_to_os_path(assert(path)) - local adapter = assert(config.get_adapter_by_scheme(scheme)) - if adapter.name == "files" then - table.insert(creates, path) - end - elseif action.type == "delete" then - local scheme, path = util.parse_url(action.url) - path = fs.posix_to_os_path(assert(path)) - local adapter = assert(config.get_adapter_by_scheme(scheme)) - if adapter.name == "files" then - table.insert(deletes, path) - end - elseif action.type == "copy" then - local scheme, path = util.parse_url(action.dest_url) - path = fs.posix_to_os_path(assert(path)) - local adapter = assert(config.get_adapter_by_scheme(scheme)) - if adapter.name == "files" then - table.insert(creates, path) - end - end - end - - local buf_was_modified = {} - for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do - buf_was_modified[bufnr] = vim.bo[bufnr].modified - end - - local edited_uris = {} - local final_err = nil - ---@param edits nil|{edit: lsp.WorkspaceEdit, client_offset: string}[] - local function accum(edits, err) - final_err = final_err or err - if edits then - for _, edit in ipairs(edits) do - if edit.edit.changes then - for uri in pairs(edit.edit.changes) do - edited_uris[uri] = true - end - end - if edit.edit.documentChanges then - for _, change in ipairs(edit.edit.documentChanges) do - if change.textDocument then - edited_uris[change.textDocument.uri] = true - end - end - end - end - end - end - local timeout_ms = config.lsp_file_methods.timeout_ms - accum(workspace.will_create_files(creates, { timeout_ms = timeout_ms })) - accum(workspace.will_delete_files(deletes, { timeout_ms = timeout_ms })) - accum(workspace.will_rename_files(moves, { timeout_ms = timeout_ms })) - if final_err then - vim.notify( - string.format("[lsp] file operation error: %s", vim.inspect(final_err)), - vim.log.levels.WARN - ) - end - - return function() - workspace.did_create_files(creates) - workspace.did_delete_files(deletes) - workspace.did_rename_files(moves) - - local autosave = config.lsp_file_methods.autosave_changes - if autosave == false then - return - end - for uri, _ in pairs(edited_uris) do - local bufnr = vim.uri_to_bufnr(uri) - local was_open = buf_was_modified[bufnr] ~= nil - local was_modified = buf_was_modified[bufnr] - local should_save = autosave == true or (autosave == "unmodified" and not was_modified) - -- Autosave changed buffers if they were not modified before - if should_save then - vim.api.nvim_buf_call(bufnr, function() - vim.cmd.update({ mods = { emsg_silent = true, noautocmd = true } }) - end) - - -- Delete buffers that weren't open before - if not was_open then - vim.api.nvim_buf_delete(bufnr, { force = true }) - end - end - end - end -end - -return M diff --git a/lua/oil/lsp/workspace.lua b/lua/oil/lsp/workspace.lua deleted file mode 100644 index 8e48276..0000000 --- a/lua/oil/lsp/workspace.lua +++ /dev/null @@ -1,351 +0,0 @@ -local fs = require("oil.fs") -local ms = require("vim.lsp.protocol").Methods -if vim.fn.has("nvim-0.10") == 0 then - ms = { - workspace_willCreateFiles = "workspace/willCreateFiles", - workspace_didCreateFiles = "workspace/didCreateFiles", - workspace_willDeleteFiles = "workspace/willDeleteFiles", - workspace_didDeleteFiles = "workspace/didDeleteFiles", - workspace_willRenameFiles = "workspace/willRenameFiles", - workspace_didRenameFiles = "workspace/didRenameFiles", - } -end - -local M = {} - ----@param method string ----@return vim.lsp.Client[] -local function get_clients(method) - if vim.fn.has("nvim-0.10") == 1 then - return vim.lsp.get_clients({ method = method }) - else - ---@diagnostic disable-next-line: deprecated - local clients = vim.lsp.get_active_clients() - return vim.tbl_filter(function(client) - return client.supports_method(method) - end, clients) - end -end - ----@param glob string|vim.lpeg.Pattern ----@param path string ----@return boolean -local function match_glob(glob, path) - -- nvim-0.10 will have vim.glob.to_lpeg, so this will be a LPeg pattern - if type(glob) ~= "string" then - return glob:match(path) ~= nil - end - - -- older versions fall back to glob2regpat - local pat = vim.fn.glob2regpat(glob) - local ignorecase = vim.o.ignorecase - vim.o.ignorecase = false - local ok, match = pcall(vim.fn.match, path, pat) - vim.o.ignorecase = ignorecase - if not ok then - error(match) - end - return match >= 0 -end - ----@param client vim.lsp.Client ----@param filters nil|lsp.FileOperationFilter[] ----@param paths string[] ----@return nil|string[] -local function get_matching_paths(client, filters, paths) - if not filters then - return nil - end - - local match_fns = {} - for _, filter in ipairs(filters) do - if filter.scheme == nil or filter.scheme == "file" then - local pattern = filter.pattern - local glob = pattern.glob - local ignore_case = pattern.options and pattern.options.ignoreCase - if ignore_case then - glob = glob:lower() - end - - -- Some language servers use forward slashes as path separators on Windows (LuaLS) - -- We no longer need this after 0.12: https://github.com/neovim/neovim/commit/322a6d305d088420b23071c227af07b7c1beb41a - if vim.fn.has("nvim-0.12") == 0 and fs.is_windows then - glob = glob:gsub("/", "\\") - end - - ---@type string|vim.lpeg.Pattern - local glob_to_match = glob - if vim.glob and vim.glob.to_lpeg then - glob = glob:gsub("{(.-)}", function(s) - local patterns = vim.split(s, ",") - local filtered = {} - for _, pat in ipairs(patterns) do - if pat ~= "" then - table.insert(filtered, pat) - end - end - if #filtered == 0 then - return "" - end - -- HACK around https://github.com/neovim/neovim/issues/28931 - -- find alternations and sort them by length to try to match the longest first - if vim.fn.has("nvim-0.11") == 0 then - table.sort(filtered, function(a, b) - return a:len() > b:len() - end) - end - return "{" .. table.concat(filtered, ",") .. "}" - end) - - glob_to_match = vim.glob.to_lpeg(glob) - end - local matches = pattern.matches - table.insert(match_fns, function(path) - local is_dir = vim.fn.isdirectory(path) == 1 - if matches and ((matches == "file" and is_dir) or (matches == "folder" and not is_dir)) then - return false - end - - if ignore_case then - path = path:lower() - end - return match_glob(glob_to_match, path) - end) - end - end - local function match_any_pattern(workspace, path) - local relative_path = path:sub(workspace:len() + 2) - for _, match_fn in ipairs(match_fns) do - -- The glob pattern might be relative to workspace OR absolute - if match_fn(relative_path) or match_fn(path) then - return true - end - end - return false - end - - local workspace_folders = vim.tbl_map(function(folder) - return vim.uri_to_fname(folder.uri) - end, client.workspace_folders or {}) - local function get_matching_workspace(path) - for _, workspace in ipairs(workspace_folders) do - if fs.is_subpath(workspace, path) then - return workspace - end - end - end - - local ret = {} - for _, path in ipairs(paths) do - local workspace = get_matching_workspace(path) - if workspace and match_any_pattern(workspace, path) then - table.insert(ret, path) - end - end - if vim.tbl_isempty(ret) then - return nil - else - return ret - end -end - ----@param method string The method to call ----@param capability_name string The name of the fileOperations server capability ----@param files string[] The files and folders that will be created ----@param options table|nil ----@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[] ----@return nil|string|lsp.ResponseError err -local function will_file_operation(method, capability_name, files, options) - options = options or {} - local clients = get_clients(method) - - local edits = {} - for _, client in ipairs(clients) do - local filters = vim.tbl_get( - client.server_capabilities, - "workspace", - "fileOperations", - capability_name, - "filters" - ) - local matching_files = get_matching_paths(client, filters, files) - if matching_files then - local params = { - files = vim.tbl_map(function(file) - return { - uri = vim.uri_from_fname(file), - } - end, matching_files), - } - local result, err - if vim.fn.has("nvim-0.11") == 1 then - result, err = client:request_sync(method, params, options.timeout_ms or 1000, 0) - else - ---@diagnostic disable-next-line: param-type-mismatch - result, err = client.request_sync(method, params, options.timeout_ms or 1000, 0) - end - if result and result.result then - if options.apply_edits ~= false then - vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding) - end - table.insert(edits, { edit = result.result, offset_encoding = client.offset_encoding }) - else - return nil, err or result and result.err - end - end - end - return edits -end - ----@param method string The method to call ----@param capability_name string The name of the fileOperations server capability ----@param files string[] The files and folders that will be created -local function did_file_operation(method, capability_name, files) - local clients = get_clients(method) - for _, client in ipairs(clients) do - local filters = vim.tbl_get( - client.server_capabilities, - "workspace", - "fileOperations", - capability_name, - "filters" - ) - local matching_files = get_matching_paths(client, filters, files) - if matching_files then - local params = { - files = vim.tbl_map(function(file) - return { - uri = vim.uri_from_fname(file), - } - end, matching_files), - } - if vim.fn.has("nvim-0.11") == 1 then - client:notify(method, params) - else - ---@diagnostic disable-next-line: param-type-mismatch - client.notify(method, params) - end - end - end -end - ---- Notify the server that the client is about to create files. ----@param files string[] The files and folders that will be created ----@param options table|nil Optional table which holds the following optional fields: ---- - timeout_ms (integer|nil, default 1000): ---- Time in milliseconds to block for rename requests. ---- - apply_edits (boolean|nil, default true): ---- Apply any workspace edits from these file operations. ----@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[] ----@return nil|string|lsp.ResponseError err -function M.will_create_files(files, options) - return will_file_operation(ms.workspace_willCreateFiles, "willCreate", files, options) -end - ---- Notify the server that files were created from within the client. ----@param files string[] The files and folders that will be created -function M.did_create_files(files) - did_file_operation(ms.workspace_didCreateFiles, "didCreate", files) -end - ---- Notify the server that the client is about to delete files. ----@param files string[] The files and folders that will be deleted ----@param options table|nil Optional table which holds the following optional fields: ---- - timeout_ms (integer|nil, default 1000): ---- Time in milliseconds to block for rename requests. ---- - apply_edits (boolean|nil, default true): ---- Apply any workspace edits from these file operations. ----@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[] ----@return nil|string|lsp.ResponseError err -function M.will_delete_files(files, options) - return will_file_operation(ms.workspace_willDeleteFiles, "willDelete", files, options) -end - ---- Notify the server that files were deleted from within the client. ----@param files string[] The files and folders that were deleted -function M.did_delete_files(files) - did_file_operation(ms.workspace_didDeleteFiles, "didDelete", files) -end - ---- Notify the server that the client is about to rename files. ----@param files table Mapping of old_path -> new_path ----@param options table|nil Optional table which holds the following optional fields: ---- - timeout_ms (integer|nil, default 1000): ---- Time in milliseconds to block for rename requests. ---- - apply_edits (boolean|nil, default true): ---- Apply any workspace edits from these file operations. ----@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[] ----@return nil|string|lsp.ResponseError err -function M.will_rename_files(files, options) - options = options or {} - local clients = get_clients(ms.workspace_willRenameFiles) - - local edits = {} - for _, client in ipairs(clients) do - local filters = vim.tbl_get( - client.server_capabilities, - "workspace", - "fileOperations", - "willRename", - "filters" - ) - local matching_files = get_matching_paths(client, filters, vim.tbl_keys(files)) - if matching_files then - local params = { - files = vim.tbl_map(function(src_file) - return { - oldUri = vim.uri_from_fname(src_file), - newUri = vim.uri_from_fname(files[src_file]), - } - end, matching_files), - } - local result, err - if vim.fn.has("nvim-0.11") == 1 then - result, err = - client:request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0) - else - result, err = - ---@diagnostic disable-next-line: param-type-mismatch - client.request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0) - end - if result and result.result then - if options.apply_edits ~= false then - vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding) - end - table.insert(edits, { edit = result.result, offset_encoding = client.offset_encoding }) - else - return nil, err or result and result.err - end - end - end - return edits -end - ---- Notify the server that files were renamed from within the client. ----@param files table Mapping of old_path -> new_path -function M.did_rename_files(files) - local clients = get_clients(ms.workspace_didRenameFiles) - for _, client in ipairs(clients) do - local filters = - vim.tbl_get(client.server_capabilities, "workspace", "fileOperations", "didRename", "filters") - local matching_files = get_matching_paths(client, filters, vim.tbl_keys(files)) - if matching_files then - local params = { - files = vim.tbl_map(function(src_file) - return { - oldUri = vim.uri_from_fname(src_file), - newUri = vim.uri_from_fname(files[src_file]), - } - end, matching_files), - } - if vim.fn.has("nvim-0.11") == 1 then - client:notify(ms.workspace_didRenameFiles, params) - else - ---@diagnostic disable-next-line: param-type-mismatch - client.notify(ms.workspace_didRenameFiles, params) - end - end - end -end - -return M diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua index f55957d..cca4bcb 100644 --- a/lua/oil/mutator/init.lua +++ b/lua/oil/mutator/init.lua @@ -1,14 +1,13 @@ -local Progress = require("oil.mutator.progress") -local Trie = require("oil.mutator.trie") local cache = require("oil.cache") local columns = require("oil.columns") local config = require("oil.config") -local confirmation = require("oil.mutator.confirmation") local constants = require("oil.constants") -local fs = require("oil.fs") -local lsp_helpers = require("oil.lsp.helpers") local oil = require("oil") local parser = require("oil.mutator.parser") +local pathutil = require("oil.pathutil") +local preview = require("oil.mutator.preview") +local Progress = require("oil.mutator.progress") +local Trie = require("oil.mutator.trie") local util = require("oil.util") local view = require("oil.view") local M = {} @@ -18,30 +17,30 @@ local FIELD_TYPE = constants.FIELD_TYPE ---@alias oil.Action oil.CreateAction|oil.DeleteAction|oil.MoveAction|oil.CopyAction|oil.ChangeAction ----@class (exact) oil.CreateAction +---@class oil.CreateAction ---@field type "create" ---@field url string ---@field entry_type oil.EntryType ---@field link nil|string ----@class (exact) oil.DeleteAction +---@class oil.DeleteAction ---@field type "delete" ---@field url string ---@field entry_type oil.EntryType ----@class (exact) oil.MoveAction +---@class oil.MoveAction ---@field type "move" ---@field entry_type oil.EntryType ---@field src_url string ---@field dest_url string ----@class (exact) oil.CopyAction +---@class oil.CopyAction ---@field type "copy" ---@field entry_type oil.EntryType ---@field src_url string ---@field dest_url string ----@class (exact) oil.ChangeAction +---@class oil.ChangeAction ---@field type "change" ---@field entry_type oil.EntryType ---@field url string @@ -54,7 +53,6 @@ M.create_actions_from_diffs = function(all_diffs) ---@type oil.Action[] local actions = {} - ---@type table local diff_by_id = setmetatable({}, { __index = function(t, key) local list = {} @@ -62,30 +60,8 @@ M.create_actions_from_diffs = function(all_diffs) return list end, }) - - -- To deduplicate create actions - -- This can happen when creating deep nested files e.g. - -- > foo/bar/a.txt - -- > foo/bar/b.txt - local seen_creates = {} - - ---@param action oil.Action - local function add_action(action) - local adapter = assert(config.get_adapter_by_scheme(action.dest_url or action.url)) - if not adapter.filter_action or adapter.filter_action(action) then - if action.type == "create" then - if seen_creates[action.url] then - return - else - seen_creates[action.url] = true - end - end - - table.insert(actions, action) - end - end for bufnr, diffs in pairs(all_diffs) do - local adapter = util.get_adapter(bufnr, true) + local adapter = util.get_adapter(bufnr) if not adapter then error("Missing adapter") end @@ -94,14 +70,12 @@ M.create_actions_from_diffs = function(all_diffs) if diff.type == "new" then if diff.id then local by_id = diff_by_id[diff.id] - ---HACK: set the destination on this diff for use later - ---@diagnostic disable-next-line: inject-field + -- FIXME this is kind of a hack. We shouldn't be setting undocumented fields on the diff diff.dest = parent_url .. diff.name table.insert(by_id, diff) else -- Parse nested files like foo/bar/baz - local path_sep = fs.is_windows and "[/\\]" or "/" - local pieces = vim.split(diff.name, path_sep) + local pieces = vim.split(diff.name, "/") local url = parent_url:gsub("/$", "") for i, v in ipairs(pieces) do local is_last = i == #pieces @@ -111,7 +85,7 @@ M.create_actions_from_diffs = function(all_diffs) -- Parse alternations like foo.{js,test.js} for _, alt in ipairs(vim.split(alternation, ",")) do local alt_url = url .. "/" .. v:gsub("{[^}]+}", alt) - add_action({ + table.insert(actions, { type = "create", url = alt_url, entry_type = entry_type, @@ -120,7 +94,7 @@ M.create_actions_from_diffs = function(all_diffs) end else url = url .. "/" .. v - add_action({ + table.insert(actions, { type = "create", url = url, entry_type = entry_type, @@ -130,7 +104,7 @@ M.create_actions_from_diffs = function(all_diffs) end end elseif diff.type == "change" then - add_action({ + table.insert(actions, { type = "change", url = parent_url .. diff.name, entry_type = diff.entry_type, @@ -139,10 +113,8 @@ M.create_actions_from_diffs = function(all_diffs) }) else local by_id = diff_by_id[diff.id] - -- HACK: set has_delete field on a list-like table of diffs - ---@diagnostic disable-next-line: inject-field by_id.has_delete = true - -- Don't insert the delete. We already know that there is a delete because of the presence + -- Don't insert the delete. We already know that there is a delete because of the presense -- in the diff_by_id map. The list will only include the 'new' diffs. end end @@ -153,25 +125,21 @@ M.create_actions_from_diffs = function(all_diffs) if not entry then error(string.format("Could not find entry %d", id)) end - ---HACK: access the has_delete field on the list-like table of diffs - ---@diagnostic disable-next-line: undefined-field if diffs.has_delete then local has_create = #diffs > 0 if has_create then -- MOVE (+ optional copies) when has both creates and delete for i, diff in ipairs(diffs) do - add_action({ + table.insert(actions, { type = i == #diffs and "move" or "copy", entry_type = entry[FIELD_TYPE], - ---HACK: access the dest field we set above - ---@diagnostic disable-next-line: undefined-field dest_url = diff.dest, src_url = cache.get_parent_url(id) .. entry[FIELD_NAME], }) end else -- DELETE when no create - add_action({ + table.insert(actions, { type = "delete", entry_type = entry[FIELD_TYPE], url = cache.get_parent_url(id) .. entry[FIELD_NAME], @@ -180,12 +148,10 @@ M.create_actions_from_diffs = function(all_diffs) else -- COPY when create but no delete for _, diff in ipairs(diffs) do - add_action({ + table.insert(actions, { type = "copy", entry_type = entry[FIELD_TYPE], src_url = cache.get_parent_url(id) .. entry[FIELD_NAME], - ---HACK: access the dest field we set above - ---@diagnostic disable-next-line: undefined-field dest_url = diff.dest, }) end @@ -218,13 +184,12 @@ M.enforce_action_order = function(actions) -- a. TODO optimization: check immediate parents to see if they have no dependencies now -- 5. repeat - ---Gets the dependencies of a particular action. Effectively dynamically calculates the dependency - ---"edges" of the graph. - ---@param action oil.Action + -- Gets the dependencies of a particular action. Effectively dynamically calculates the dependency + -- "edges" of the graph. local function get_deps(action) local ret = {} if action.type == "delete" then - src_trie:accum_children_of(action.url, ret) + return ret elseif action.type == "create" then -- Finish operating on parents first -- e.g. NEW /a BEFORE NEW /a/b @@ -253,10 +218,11 @@ M.enforce_action_order = function(actions) -- Process children before moving -- e.g. NEW /a/b BEFORE MOVE /a -> /b dest_trie:accum_children_of(action.src_url, ret) - -- Process children before moving parent dir + -- Copy children before moving parent dir -- e.g. COPY /a/b -> /b BEFORE MOVE /a -> /d - -- e.g. CHANGE /a/b BEFORE MOVE /a -> /d - src_trie:accum_children_of(action.src_url, ret) + src_trie:accum_children_of(action.src_url, ret, function(a) + return a.type == "copy" + end) -- Process remove path before moving to new path -- e.g. MOVE /a -> /b BEFORE MOVE /c -> /a src_trie:accum_actions_at(action.dest_url, ret, function(a) @@ -381,28 +347,36 @@ M.enforce_action_order = function(actions) return ret end -local progress - ---@param actions oil.Action[] ---@param cb fun(err: nil|string) M.process_actions = function(actions, cb) - vim.api.nvim_exec_autocmds( - "User", - { pattern = "OilActionsPre", modeline = false, data = { actions = actions } } - ) - - local did_complete = nil - if config.lsp_file_methods.enabled then - did_complete = lsp_helpers.will_perform_file_operations(actions) + -- convert delete actions to move-to-trash + local trash_url = config.get_trash_url() + if trash_url then + for i, v in ipairs(actions) do + if v.type == "delete" then + local scheme, path = util.parse_url(v.url) + if config.adapters[scheme] == "files" then + actions[i] = { + type = "move", + src_url = v.url, + entry_type = v.entry_type, + dest_url = trash_url .. "/" .. pathutil.basename(path) .. string.format( + "_%06d", + math.random(999999) + ), + } + end + end + end end - -- Convert some cross-adapter moves to a copy + delete + -- Convert cross-adapter moves to a copy + delete for _, action in ipairs(actions) do if action.type == "move" then - local _, cross_action = util.get_adapter_for_action(action) - -- Only do the conversion if the cross-adapter support is "copy" - if cross_action == "copy" then - ---@diagnostic disable-next-line: assign-type-mismatch + local src_scheme = util.parse_url(action.src_url) + local dest_scheme = util.parse_url(action.dest_url) + if src_scheme ~= dest_scheme then action.type = "copy" table.insert(actions, { type = "delete", @@ -414,17 +388,12 @@ M.process_actions = function(actions, cb) end local finished = false - progress = Progress.new() - local function finish(err) + local progress = Progress.new() + local function finish(...) if not finished then finished = true progress:close() - progress = nil - vim.api.nvim_exec_autocmds( - "User", - { pattern = "OilActionsPost", modeline = false, data = { err = err, actions = actions } } - ) - cb(err) + cb(...) end end @@ -448,9 +417,6 @@ M.process_actions = function(actions, cb) return end if idx > #actions then - if did_complete then - did_complete() - end finish() return end @@ -473,7 +439,6 @@ M.process_actions = function(actions, cb) end end) if action.type == "change" then - ---@cast action oil.ChangeAction columns.perform_change_action(adapter, action, callback) else adapter.perform_action(action, callback) @@ -482,34 +447,18 @@ M.process_actions = function(actions, cb) next_action() end -M.show_progress = function() - if progress then - progress:restore() - end -end - local mutation_in_progress = false ----@return boolean -M.is_mutating = function() - return mutation_in_progress -end - ---@param confirm nil|boolean ----@param cb? fun(err: nil|string) -M.try_write_changes = function(confirm, cb) - if not cb then - cb = function(_err) end - end +M.try_write_changes = function(confirm) if mutation_in_progress then - cb("Cannot perform mutation when already in progress") + error("Cannot perform mutation when already in progress") return end local current_buf = vim.api.nvim_get_current_buf() local was_modified = vim.bo.modified local buffers = view.get_all_buffers() local all_diffs = {} - ---@type table local all_errors = {} mutation_in_progress = true @@ -519,10 +468,6 @@ M.try_write_changes = function(confirm, cb) if vim.bo[bufnr].modified then local diffs, errors = parser.parse(bufnr) all_diffs[bufnr] = diffs - local adapter = assert(util.get_adapter(bufnr, true)) - if adapter.filter_error then - errors = vim.tbl_filter(adapter.filter_error, errors) - end if not vim.tbl_isempty(errors) then all_errors[bufnr] = errors end @@ -540,6 +485,7 @@ M.try_write_changes = function(confirm, cb) local ns = vim.api.nvim_create_namespace("Oil") vim.diagnostic.reset(ns) if not vim.tbl_isempty(all_errors) then + vim.notify("Error parsing oil buffers", vim.log.levels.ERROR) for bufnr, errors in pairs(all_errors) do vim.diagnostic.set(ns, bufnr, errors) end @@ -553,27 +499,17 @@ M.try_write_changes = function(confirm, cb) { all_errors[curbuf][1].lnum + 1, all_errors[curbuf][1].col } ) else - local bufnr, errs = next(all_errors) - assert(bufnr) - assert(errs) - -- HACK: This is a workaround for the fact that we can't switch buffers in the middle of a - -- BufWriteCmd. - vim.schedule(function() - vim.api.nvim_win_set_buf(0, bufnr) - pcall(vim.api.nvim_win_set_cursor, 0, { errs[1].lnum + 1, errs[1].col }) - end) + local bufnr, errs = next(pairs(all_errors)) + vim.api.nvim_win_set_buf(0, bufnr) + pcall(vim.api.nvim_win_set_cursor, 0, { errs[1].lnum + 1, errs[1].col }) end - unlock() - cb("Error parsing oil buffers") - return + return unlock() end local actions = M.create_actions_from_diffs(all_diffs) - confirmation.show(actions, confirm, function(proceed) + preview.show(actions, confirm, function(proceed) if not proceed then - unlock() - cb("Canceled") - return + return unlock() end M.process_actions( @@ -581,10 +517,8 @@ M.try_write_changes = function(confirm, cb) vim.schedule_wrap(function(err) view.unlock_buffers() if err then - err = string.format("[oil] Error applying actions: %s", err) - view.rerender_all_oil_buffers(nil, function() - cb(err) - end) + vim.notify(string.format("[oil] Error applying actions: %s", err), vim.log.levels.ERROR) + view.rerender_all_oil_buffers({ preserve_undo = false }) else local current_entry = oil.get_cursor_entry() if current_entry then @@ -594,13 +528,7 @@ M.try_write_changes = function(confirm, cb) vim.split(current_entry.parsed_name or current_entry.name, "/")[1] ) end - view.rerender_all_oil_buffers(nil, function(render_err) - vim.api.nvim_exec_autocmds( - "User", - { pattern = "OilMutationComplete", modeline = false } - ) - cb(render_err) - end) + view.rerender_all_oil_buffers({ preserve_undo = M.trash }) end mutation_in_progress = false end) diff --git a/lua/oil/mutator/parser.lua b/lua/oil/mutator/parser.lua index bb22274..60d163a 100644 --- a/lua/oil/mutator/parser.lua +++ b/lua/oil/mutator/parser.lua @@ -1,6 +1,5 @@ local cache = require("oil.cache") local columns = require("oil.columns") -local config = require("oil.config") local constants = require("oil.constants") local fs = require("oil.fs") local util = require("oil.util") @@ -14,19 +13,19 @@ local FIELD_META = constants.FIELD_META ---@alias oil.Diff oil.DiffNew|oil.DiffDelete|oil.DiffChange ----@class (exact) oil.DiffNew +---@class oil.DiffNew ---@field type "new" ---@field name string ---@field entry_type oil.EntryType ---@field id nil|integer ---@field link nil|string ----@class (exact) oil.DiffDelete +---@class oil.DiffDelete ---@field type "delete" ---@field name string ---@field id integer - ----@class (exact) oil.DiffChange +--- +---@class oil.DiffChange ---@field type "change" ---@field entry_type oil.EntryType ---@field name string @@ -37,7 +36,7 @@ local FIELD_META = constants.FIELD_META ---@return string ---@return boolean local function parsedir(name) - local isdir = vim.endswith(name, "/") or (fs.is_windows and vim.endswith(name, "\\")) + local isdir = vim.endswith(name, "/") if isdir then name = name:sub(1, name:len() - 1) end @@ -57,7 +56,7 @@ local function compare_link_target(meta, parsed_entry) return meta_name == parsed_name end ----@class (exact) oil.ParseResult +---@class oil.ParseResult ---@field data table Parsed entry data ---@field ranges table Locations of the various columns ---@field entry nil|oil.InternalEntry If the entry already exists @@ -79,24 +78,12 @@ M.parse_line = function(adapter, line, column_defs) ranges.id = { start, value:len() + 1 } start = ranges.id[2] + 1 ret.id = tonumber(value) - - -- Right after a mutation and we reset the cache, the parent url may not be available - local ok, parent_url = pcall(cache.get_parent_url, ret.id) - if ok then - -- If this line was pasted from another adapter, it may have different columns - local line_adapter = assert(config.get_adapter_by_scheme(parent_url)) - if adapter ~= line_adapter then - adapter = line_adapter - column_defs = columns.get_supported_columns(adapter) - end - end - for _, def in ipairs(column_defs) do local name = util.split_config(def) local range = { start } local start_len = string.len(rem) - value, rem = columns.parse_col(adapter, assert(rem), def) - if not rem then + value, rem = columns.parse_col(adapter, rem, def) + if not value or not rem then return nil, string.format("Parsing %s failed", name) end ret[name] = value @@ -142,21 +129,14 @@ M.parse_line = function(adapter, line, column_defs) return { data = ret, entry = entry, ranges = ranges } end ----@class (exact) oil.ParseError ----@field lnum integer ----@field col integer ----@field message string - ---@param bufnr integer ----@return oil.Diff[] diffs ----@return oil.ParseError[] errors Parsing errors +---@return oil.Diff[] +---@return table[] Parsing errors M.parse = function(bufnr) - ---@type oil.Diff[] local diffs = {} - ---@type oil.ParseError[] local errors = {} local bufname = vim.api.nvim_buf_get_name(bufnr) - local adapter = util.get_adapter(bufnr, true) + local adapter = util.get_adapter(bufnr) if not adapter then table.insert(errors, { lnum = 0, @@ -165,19 +145,15 @@ M.parse = function(bufnr) }) return diffs, errors end - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) local scheme, path = util.parse_url(bufname) - local column_defs = columns.get_supported_columns(adapter) local parent_url = scheme .. path + local column_defs = columns.get_supported_columns(adapter) local children = cache.list_url(parent_url) - -- map from name to entry ID for all entries previously in the buffer - ---@type table + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) local original_entries = {} for _, child in pairs(children) do - local name = child[FIELD_NAME] - if view.should_display(name, bufnr) then - original_entries[name] = child[FIELD_ID] + if view.should_display(child, bufnr) then + original_entries[child[FIELD_NAME]] = child[FIELD_ID] end end local seen_names = {} @@ -187,124 +163,103 @@ M.parse = function(bufnr) name = name:lower() end 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, col = 0 }) else seen_names[name] = true end end - for i, line in ipairs(lines) do - -- hack to be compatible with Lua 5.1 - -- use return instead of goto - (function() - if line:match("^/%d+") then - -- Parse the line for an existing entry - local result, err = M.parse_line(adapter, line, column_defs) - if not result or err then - table.insert(errors, { - message = err, - lnum = i - 1, - end_lnum = i, - col = 0, - }) - return - elseif result.data.id == 0 then - -- Ignore entries with ID 0 (typically the "../" entry) - return - end - local parsed_entry = result.data - local entry = result.entry - - local err_message + if line:match("^/%d+") then + local result, err = M.parse_line(adapter, line, column_defs) + if not result or err then + table.insert(errors, { + message = err, + lnum = i - 1, + col = 0, + }) + goto continue + end + local parsed_entry = result.data + local entry = result.entry + if not parsed_entry.name or parsed_entry.name:match("/") or not entry then + local message if not parsed_entry.name then - err_message = "No filename found" + message = "No filename found" elseif not entry then - err_message = "Could not find existing entry (was the ID changed?)" - elseif parsed_entry.name:match("/") or parsed_entry.name:match(fs.sep) then - err_message = "Filename cannot contain path separator" - end - if err_message then - table.insert(errors, { - message = err_message, - lnum = i - 1, - end_lnum = i, - col = 0, - }) - return - end - assert(entry) - - check_dupe(parsed_entry.name, i) - local meta = entry[FIELD_META] - if original_entries[parsed_entry.name] == parsed_entry.id then - if entry[FIELD_TYPE] == "link" and not compare_link_target(meta, parsed_entry) then - table.insert(diffs, { - type = "new", - name = parsed_entry.name, - entry_type = "link", - link = parsed_entry.link_target, - }) - elseif entry[FIELD_TYPE] ~= parsed_entry._type then - table.insert(diffs, { - type = "new", - name = parsed_entry.name, - entry_type = parsed_entry._type, - }) - else - original_entries[parsed_entry.name] = nil - end + message = "Could not find existing entry (was the ID changed?)" else + message = "Filename cannot contain '/'" + end + table.insert(errors, { + message = message, + lnum = i - 1, + col = 0, + }) + goto continue + end + check_dupe(parsed_entry.name, i) + local meta = entry[FIELD_META] + if original_entries[parsed_entry.name] == parsed_entry.id then + if entry[FIELD_TYPE] == "link" and not compare_link_target(meta, parsed_entry) then table.insert(diffs, { type = "new", name = parsed_entry.name, - entry_type = parsed_entry._type, - id = parsed_entry.id, + entry_type = "link", link = parsed_entry.link_target, }) - end - - for _, col_def in ipairs(column_defs) do - local col_name = util.split_config(col_def) - if columns.compare(adapter, col_name, entry, parsed_entry[col_name]) then - table.insert(diffs, { - type = "change", - name = parsed_entry.name, - entry_type = entry[FIELD_TYPE], - column = col_name, - value = parsed_entry[col_name], - }) - end + else + original_entries[parsed_entry.name] = nil end else - -- Parse a new entry - local name, isdir = parsedir(vim.trim(line)) - if vim.startswith(name, "/") then - table.insert(errors, { - message = "Paths cannot start with '/'", - lnum = i - 1, - end_lnum = i, - col = 0, - }) - return - end - if name ~= "" then - local link_pieces = vim.split(name, " -> ", { plain = true }) - local entry_type = isdir and "directory" or "file" - local link - if #link_pieces == 2 then - entry_type = "link" - name, link = unpack(link_pieces) - end - check_dupe(name, i) + table.insert(diffs, { + type = "new", + name = parsed_entry.name, + entry_type = parsed_entry._type, + id = parsed_entry.id, + link = parsed_entry.link_target, + }) + end + + for _, col_def in ipairs(column_defs) do + local col_name = util.split_config(col_def) + if columns.compare(adapter, col_name, entry, parsed_entry[col_name]) then table.insert(diffs, { - type = "new", - name = name, - entry_type = entry_type, - link = link, + type = "change", + name = parsed_entry.name, + entry_type = entry[FIELD_TYPE], + column = col_name, + value = parsed_entry[col_name], }) end end - end)() + else + local name, isdir = parsedir(vim.trim(line)) + if vim.startswith(name, "/") then + table.insert(errors, { + message = "Paths cannot start with '/'", + lnum = i - 1, + col = 0, + }) + goto continue + end + if name ~= "" then + local link_pieces = vim.split(name, " -> ", { plain = true }) + local entry_type = isdir and "directory" or "file" + local link + if #link_pieces == 2 then + entry_type = "link" + name, link = unpack(link_pieces) + end + check_dupe(name, i) + table.insert(diffs, { + type = "new", + name = name, + entry_type = entry_type, + link = link, + }) + end + end + ::continue:: end for name, child_id in pairs(original_entries) do diff --git a/lua/oil/mutator/confirmation.lua b/lua/oil/mutator/preview.lua similarity index 83% rename from lua/oil/mutator/confirmation.lua rename to lua/oil/mutator/preview.lua index 8bc8020..54297e8 100644 --- a/lua/oil/mutator/confirmation.lua +++ b/lua/oil/mutator/preview.lua @@ -45,12 +45,11 @@ end ---@param bufnr integer ---@param lines string[] local function render_lines(winid, bufnr, lines) - util.render_text(bufnr, lines, { - v_align = "top", - h_align = "left", - winid = winid, - actions = { "[Y]es", "[N]o" }, - }) + util.render_text( + bufnr, + lines, + { v_align = "top", h_align = "left", winid = winid, actions = { "[O]k", "[C]ancel" } } + ) end ---@param actions oil.Action[] @@ -77,13 +76,10 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) local adapter = util.get_adapter_for_action(action) local line if action.type == "change" then - ---@cast action oil.ChangeAction line = columns.render_change_action(adapter, action) else line = adapter.render_action(action) end - -- We can't handle lines with newlines in them - line = line:gsub("\n", "") table.insert(lines, line) local line_width = vim.api.nvim_strwidth(line) if line_width > max_line_width then @@ -93,7 +89,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) table.insert(lines, "") -- Create the floating window - local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.confirmation) + local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.preview) local ok, winid = pcall(vim.api.nvim_open_win, bufnr, true, { relative = "editor", width = width, @@ -102,7 +98,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) col = math.floor((layout.get_editor_width() - width) / 2), zindex = 152, -- render on top of the floating window title style = "minimal", - border = config.confirmation.border, + border = config.preview.border, }) if not ok then vim.notify(string.format("Error showing oil preview window: %s", winid), vim.log.levels.ERROR) @@ -110,14 +106,12 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) end vim.bo[bufnr].filetype = "oil_preview" vim.bo[bufnr].syntax = "oil_preview" - for k, v in pairs(config.confirmation.win_options) do + for k, v in pairs(config.preview.win_options) do vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) end render_lines(winid, bufnr, lines) - local restore_cursor = util.hide_cursor() - -- Attach autocmds and keymaps local cancel local confirm @@ -131,7 +125,6 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) end autocmds = {} vim.api.nvim_win_close(winid, true) - restore_cursor() cb(value) end end @@ -157,7 +150,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) vim.api.nvim_create_autocmd("VimResized", { callback = function() if vim.api.nvim_win_is_valid(winid) then - width, height = layout.calculate_dims(max_line_width, #lines, config.confirmation) + width, height = layout.calculate_dims(max_line_width, #lines, config.preview) vim.api.nvim_win_set_config(winid, { relative = "editor", width = width, @@ -171,22 +164,17 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) end, }) ) - - -- We used to use [C]ancel to cancel, so preserve the old keymap - local cancel_keys = { "n", "N", "c", "C", "q", "", "" } - for _, cancel_key in ipairs(cancel_keys) do + for _, cancel_key in ipairs({ "q", "C", "c", "", "" }) do vim.keymap.set("n", cancel_key, function() cancel() end, { buffer = bufnr, nowait = true }) end - - -- We used to use [O]k to confirm, so preserve the old keymap - local confirm_keys = { "y", "Y", "o", "O" } - for _, confirm_key in ipairs(confirm_keys) do - vim.keymap.set("n", confirm_key, function() - confirm() - end, { buffer = bufnr, nowait = true }) - end + vim.keymap.set("n", "O", function() + confirm() + end, { buffer = bufnr }) + vim.keymap.set("n", "o", function() + confirm() + end, { buffer = bufnr }) end) return M diff --git a/lua/oil/mutator/progress.lua b/lua/oil/mutator/progress.lua index 057f0a0..d208c2d 100644 --- a/lua/oil/mutator/progress.lua +++ b/lua/oil/mutator/progress.lua @@ -1,5 +1,5 @@ -local columns = require("oil.columns") local config = require("oil.config") +local columns = require("oil.columns") local layout = require("oil.layout") local loading = require("oil.loading") local util = require("oil.util") @@ -8,11 +8,13 @@ local Progress = {} local FPS = 20 function Progress.new() + local bufnr = vim.api.nvim_create_buf(false, true) + vim.bo[bufnr].bufhidden = "wipe" return setmetatable({ lines = { "", "" }, count = "", spinner = "", - bufnr = nil, + bufnr = bufnr, winid = nil, min_bufnr = nil, min_winid = nil, @@ -23,15 +25,6 @@ function Progress.new() }) end ----@private ----@return boolean -function Progress:is_minimized() - return not self.closing - and not self.bufnr - and self.min_bufnr - and vim.api.nvim_buf_is_valid(self.min_bufnr) -end - ---@param opts nil|table --- cancel fun() function Progress:show(opts) @@ -39,24 +32,20 @@ function Progress:show(opts) if self.winid and vim.api.nvim_win_is_valid(self.winid) then return end - local bufnr = vim.api.nvim_create_buf(false, true) - vim.bo[bufnr].bufhidden = "wipe" - self.bufnr = bufnr - self.cancel = opts.cancel or self.cancel + self.closing = false + self.cancel = opts.cancel local loading_iter = loading.get_bar_iter() local spinner = loading.get_iter("dots") - if not self.timer then - self.timer = vim.loop.new_timer() - self.timer:start( - 0, - math.floor(1000 / FPS), - vim.schedule_wrap(function() - self.lines[2] = string.format("%s %s", self.count, loading_iter()) - self.spinner = spinner() - self:_render() - end) - ) - end + self.timer = vim.loop.new_timer() + self.timer:start( + 0, + math.floor(1000 / FPS), + vim.schedule_wrap(function() + self.lines[2] = string.format("%s %s", self.count, loading_iter()) + self.spinner = spinner() + self:_render() + end) + ) local width, height = layout.calculate_dims(120, 10, config.progress) self.winid = vim.api.nvim_open_win(self.bufnr, true, { relative = "editor", @@ -69,7 +58,7 @@ function Progress:show(opts) border = config.progress.border, }) vim.bo[self.bufnr].filetype = "oil_progress" - for k, v in pairs(config.progress.win_options) do + for k, v in pairs(config.preview.win_options) do vim.api.nvim_set_option_value(k, v, { scope = "local", win = self.winid }) end table.insert( @@ -100,16 +89,6 @@ function Progress:show(opts) vim.keymap.set("n", "M", minimize, { buffer = self.bufnr, nowait = true }) end -function Progress:restore() - if self.closing then - return - elseif not self:is_minimized() then - error("Cannot restore progress window: not minimized") - end - self:_cleanup_minimized_win() - self:show() -end - function Progress:_render() if self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr) then util.render_text( @@ -160,14 +139,6 @@ function Progress:_cleanup_main_win() self.bufnr = nil end -function Progress:_cleanup_minimized_win() - if self.min_winid and vim.api.nvim_win_is_valid(self.min_winid) then - vim.api.nvim_win_close(self.min_winid, true) - end - self.min_winid = nil - self.min_bufnr = nil -end - function Progress:minimize() if self.closing then return @@ -189,7 +160,6 @@ function Progress:minimize() self.min_bufnr = bufnr self.min_winid = winid self:_render() - vim.notify_once("Restore progress window with :Oil --progress") end ---@param action oil.Action @@ -199,7 +169,6 @@ function Progress:set_action(action, idx, total) local adapter = util.get_adapter_for_action(action) local change_line if action.type == "change" then - ---@cast action oil.ChangeAction change_line = columns.render_change_action(adapter, action) else change_line = adapter.render_action(action) @@ -217,7 +186,11 @@ function Progress:close() self.timer = nil end self:_cleanup_main_win() - self:_cleanup_minimized_win() + if self.min_winid and vim.api.nvim_win_is_valid(self.min_winid) then + vim.api.nvim_win_close(self.min_winid, true) + end + self.min_winid = nil + self.min_bufnr = nil end return Progress diff --git a/lua/oil/mutator/trie.lua b/lua/oil/mutator/trie.lua index 7bac161..cb10bbd 100644 --- a/lua/oil/mutator/trie.lua +++ b/lua/oil/mutator/trie.lua @@ -1,13 +1,7 @@ local util = require("oil.util") - ----@class (exact) oil.Trie ----@field new fun(): oil.Trie ----@field private root table local Trie = {} ----@return oil.Trie Trie.new = function() - ---@type oil.Trie return setmetatable({ root = { values = {}, children = {} }, }, { @@ -19,7 +13,6 @@ end ---@return string[] function Trie:_url_to_path_pieces(url) local scheme, path = util.parse_url(url) - assert(path) local pieces = vim.split(path, "/") table.insert(pieces, 1, scheme) return pieces @@ -118,7 +111,7 @@ end ---Add all actions affecting children of the url ---@param url string ---@param ret oil.InternalEntry[] ----@param filter nil|fun(entry: oil.Action): boolean +---@param filter nil|fun(entry: oil.InternalEntry): boolean function Trie:accum_children_of(url, ret, filter) local pieces = self:_url_to_path_pieces(url) local current = self.root @@ -138,7 +131,7 @@ end ---Add all actions at a specific path ---@param url string ---@param ret oil.InternalEntry[] ----@param filter? fun(entry: oil.Action): boolean +---@param filter nil|fun(entry: oil.InternalEntry): boolean function Trie:accum_actions_at(url, ret, filter) local pieces = self:_url_to_path_pieces(url) local current = self.root diff --git a/lua/oil/pathutil.lua b/lua/oil/pathutil.lua index b030389..1c8877c 100644 --- a/lua/oil/pathutil.lua +++ b/lua/oil/pathutil.lua @@ -1,8 +1,16 @@ +local fs = require("oil.fs") local M = {} ---@param path string ---@return string M.parent = function(path) + -- Do I love this hack? No I do not. + -- Does it work? Yes. Mostly. For now. + if fs.is_windows then + if path:match("^/%a+/?$") then + return path + end + end if path == "/" then return "/" elseif path == "" then diff --git a/lua/oil/ringbuf.lua b/lua/oil/ringbuf.lua deleted file mode 100644 index 0f09987..0000000 --- a/lua/oil/ringbuf.lua +++ /dev/null @@ -1,37 +0,0 @@ ----@class oil.Ringbuf ----@field private size integer ----@field private tail integer ----@field private buf string[] -local Ringbuf = {} -function Ringbuf.new(size) - local self = setmetatable({ - size = size, - buf = {}, - tail = 0, - }, { __index = Ringbuf }) - return self -end - ----@param val string -function Ringbuf:push(val) - self.tail = self.tail + 1 - if self.tail > self.size then - self.tail = 1 - end - self.buf[self.tail] = val -end - ----@return string -function Ringbuf:as_str() - local postfix = "" - for i = 1, self.tail, 1 do - postfix = postfix .. self.buf[i] - end - local prefix = "" - for i = self.tail + 1, #self.buf, 1 do - prefix = prefix .. self.buf[i] - end - return prefix .. postfix -end - -return Ringbuf diff --git a/lua/oil/shell.lua b/lua/oil/shell.lua index b04b27b..2c401ef 100644 --- a/lua/oil/shell.lua +++ b/lua/oil/shell.lua @@ -26,8 +26,7 @@ M.run = function(cmd, opts, callback) if err == "" then err = "Unknown error" end - local cmd_str = type(cmd) == "string" and cmd or table.concat(cmd, " ") - callback(string.format("Error running command '%s'\n%s", cmd_str, err)) + callback(err) end end), }) diff --git a/lua/oil/util.lua b/lua/oil/util.lua index 0ec1acd..ba3d140 100644 --- a/lua/oil/util.lua +++ b/lua/oil/util.lua @@ -8,8 +8,6 @@ local FIELD_NAME = constants.FIELD_NAME local FIELD_TYPE = constants.FIELD_TYPE local FIELD_META = constants.FIELD_META ----@alias oil.IconProvider fun(type: string, name: string, conf: table?): (icon: string, hl: string) - ---@param url string ---@return nil|string ---@return nil|string @@ -21,67 +19,50 @@ end ---@param filename string ---@return string M.escape_filename = function(filename) - local ret = vim.fn.fnameescape(filename) + local ret = filename:gsub("([%%#$])", "\\%1") return ret end -local _url_escape_to_char = { - ["20"] = " ", - ["22"] = "“", - ["23"] = "#", - ["24"] = "$", - ["25"] = "%", - ["26"] = "&", - ["27"] = "‘", - ["2B"] = "+", - ["2C"] = ",", - ["2F"] = "/", - ["3A"] = ":", - ["3B"] = ";", - ["3C"] = "<", - ["3D"] = "=", - ["3E"] = ">", - ["3F"] = "?", - ["40"] = "@", - ["5B"] = "[", - ["5C"] = "\\", - ["5D"] = "]", - ["5E"] = "^", - ["60"] = "`", - ["7B"] = "{", - ["7C"] = "|", - ["7D"] = "}", - ["7E"] = "~", +local _url_escape_chars = { + [" "] = "%20", + ["$"] = "%24", + ["&"] = "%26", + ["`"] = "%60", + [":"] = "%3A", + ["<"] = "%3C", + ["="] = "%3D", + [">"] = "%3E", + ["?"] = "%3F", + ["["] = "%5B", + ["\\"] = "%5C", + ["]"] = "%5D", + ["^"] = "%5E", + ["{"] = "%7B", + ["|"] = "%7C", + ["}"] = "%7D", + ["~"] = "%7E", + ["“"] = "%22", + ["‘"] = "%27", + ["+"] = "%2B", + [","] = "%2C", + ["#"] = "%23", + ["%"] = "%25", + ["@"] = "%40", + ["/"] = "%2F", + [";"] = "%3B", } -local _char_to_url_escape = {} -for k, v in pairs(_url_escape_to_char) do - _char_to_url_escape[v] = "%" .. k -end --- TODO this uri escape handling is very incomplete - ---@param string string ---@return string M.url_escape = function(string) - return (string:gsub(".", _char_to_url_escape)) -end - ----@param string string ----@return string -M.url_unescape = function(string) - return ( - string:gsub("%%([0-9A-Fa-f][0-9A-Fa-f])", function(seq) - return _url_escape_to_char[seq:upper()] or ("%" .. seq) - end) - ) + return (string:gsub(".", _url_escape_chars)) end ---@param bufnr integer ----@param silent? boolean ---@return nil|oil.Adapter -M.get_adapter = function(bufnr, silent) +M.get_adapter = function(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr) local adapter = config.get_adapter_by_scheme(bufname) - if not adapter and not silent then + if not adapter then vim.notify_once( string.format("[oil] could not find adapter for buffer '%s://'", bufname), vim.log.levels.ERROR @@ -91,28 +72,34 @@ M.get_adapter = function(bufnr, silent) end ---@param text string ----@param width integer|nil ----@param align oil.ColumnAlign ----@return string padded_text ----@return integer left_padding -M.pad_align = function(text, width, align) - if not width then - return text, 0 +---@param length nil|integer +---@return string +M.rpad = function(text, length) + if not length then + return text end - local text_width = vim.api.nvim_strwidth(text) - local total_pad = width - text_width - if total_pad <= 0 then - return text, 0 - end - - if align == "right" then - return string.rep(" ", total_pad) .. text, total_pad - elseif align == "center" then - local left_pad = math.floor(total_pad / 2) - local right_pad = total_pad - left_pad - return string.rep(" ", left_pad) .. text .. string.rep(" ", right_pad), left_pad + local textlen = vim.api.nvim_strwidth(text) + local delta = length - textlen + if delta > 0 then + return text .. string.rep(" ", delta) else - return text .. string.rep(" ", total_pad), 0 + return text + end +end + +---@param text string +---@param length nil|integer +---@return string +M.lpad = function(text, length) + if not length then + return text + end + local textlen = vim.api.nvim_strwidth(text) + local delta = length - textlen + if delta > 0 then + return string.rep(" ", delta) .. text + else + return text end end @@ -163,15 +150,12 @@ M.rename_buffer = function(src_bufnr, dest_buf_name) -- rename logic. The only reason we can't use nvim_buf_set_name on files is because vim will -- 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 - ---@diagnostic disable-next-line: param-type-mismatch local altbuf = vim.fn.bufnr("#") -- This will fail if the dest buf name already exists local ok = pcall(vim.api.nvim_buf_set_name, src_bufnr, dest_buf_name) if ok then - -- Renaming the buffer creates a new buffer with the old name. - -- Find it and try to delete it, but don't if the buffer is in a context - -- where Neovim doesn't allow buffer modifications. - pcall(vim.api.nvim_buf_delete, vim.fn.bufadd(bufname), {}) + -- Renaming the buffer creates a new buffer with the old name. Find it and delete it. + vim.api.nvim_buf_delete(vim.fn.bufadd(bufname), {}) if altbuf and vim.api.nvim_buf_is_valid(altbuf) then vim.fn.setreg("#", altbuf) end @@ -208,18 +192,6 @@ M.rename_buffer = function(src_bufnr, dest_buf_name) -- Try to delete, but don't if the buffer has changes pcall(vim.api.nvim_buf_delete, src_bufnr, {}) end - -- Renaming a buffer won't load the undo file, so we need to do that manually - if vim.bo[dest_bufnr].undofile then - vim.api.nvim_buf_call(dest_bufnr, function() - vim.cmd.rundo({ - args = { vim.fn.undofile(dest_buf_name) }, - magic = { file = false, bar = false }, - mods = { - emsg_silent = true, - }, - }) - end) - end end) return true end @@ -246,7 +218,6 @@ local function get_possible_buffer_names_from_url(url) local fs = require("oil.fs") local scheme, path = M.parse_url(url) if config.adapters[scheme] == "files" then - assert(path) return { fs.posix_to_os_path(path) } end return { url } @@ -308,15 +279,11 @@ M.split_config = function(name_or_config) end end ----@alias oil.ColumnAlign "left"|"center"|"right" - ---@param lines oil.TextChunk[][] ---@param col_width integer[] ----@param col_align? oil.ColumnAlign[] ---@return string[] ---@return any[][] List of highlights {group, lnum, col_start, col_end} -M.render_table = function(lines, col_width, col_align) - col_align = col_align or {} +M.render_table = function(lines, col_width) local str_lines = {} local highlights = {} for _, cols in ipairs(lines) do @@ -325,35 +292,17 @@ M.render_table = function(lines, col_width, col_align) for i, chunk in ipairs(cols) do local text, hl if type(chunk) == "table" then - text = chunk[1] - hl = chunk[2] + text, hl = unpack(chunk) else text = chunk end - - local unpadded_len = text:len() - local padding - text, padding = M.pad_align(text, col_width[i], col_align[i] or "left") - + text = M.rpad(text, col_width[i]) table.insert(pieces, text) + local col_end = col + text:len() + 1 if hl then - if type(hl) == "table" then - -- hl has the form { [1]: hl_name, [2]: col_start, [3]: col_end }[] - -- Notice that col_start and col_end are relative position inside - -- that col, so we need to add the offset to them - for _, sub_hl in ipairs(hl) do - table.insert(highlights, { - sub_hl[1], - #str_lines, - col + padding + sub_hl[2], - col + padding + sub_hl[3], - }) - end - else - table.insert(highlights, { hl, #str_lines, col + padding, col + padding + unpadded_len }) - end + table.insert(highlights, { hl, #str_lines, col, col_end }) end - col = col + text:len() + 1 + col = col_end end table.insert(str_lines, table.concat(pieces, " ")) end @@ -366,27 +315,15 @@ M.set_highlights = function(bufnr, highlights) local ns = vim.api.nvim_create_namespace("Oil") vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) for _, hl in ipairs(highlights) do - local group, line, col_start, col_end = unpack(hl) - vim.api.nvim_buf_set_extmark(bufnr, ns, line, col_start, { - end_col = col_end, - hl_group = group, - strict = false, - }) + vim.api.nvim_buf_add_highlight(bufnr, ns, unpack(hl)) end end ---@param path string ----@param os_slash? boolean use os filesystem slash instead of posix slash ---@return string -M.addslash = function(path, os_slash) - local slash = "/" - if os_slash and require("oil.fs").is_windows then - slash = "\\" - end - - local endslash = path:match(slash .. "$") - if not endslash then - return path .. slash +M.addslash = function(path) + if not vim.endswith(path, "/") then + return path .. "/" else return path end @@ -398,26 +335,6 @@ M.is_floating_win = function(winid) return vim.api.nvim_win_get_config(winid or 0).relative ~= "" end ----Recalculate the window title for the current buffer ----@param winid nil|integer ----@return string -M.get_title = function(winid) - if config.float.get_win_title ~= nil then - return config.float.get_win_title(winid or 0) - end - - local src_buf = vim.api.nvim_win_get_buf(winid or 0) - local title = vim.api.nvim_buf_get_name(src_buf) - local scheme, path = M.parse_url(title) - - if config.adapters[scheme] == "files" then - assert(path) - local fs = require("oil.fs") - title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~") - end - return title -end - local winid_map = {} M.add_title_to_win = function(winid, opts) opts = opts or {} @@ -425,10 +342,20 @@ M.add_title_to_win = function(winid, opts) if not vim.api.nvim_win_is_valid(winid) then return end + local function get_title() + local src_buf = vim.api.nvim_win_get_buf(winid) + local title = vim.api.nvim_buf_get_name(src_buf) + local scheme, path = M.parse_url(title) + if config.adapters[scheme] == "files" then + local fs = require("oil.fs") + title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~") + end + return title + end -- HACK to force the parent window to position itself -- See https://github.com/neovim/neovim/issues/13403 vim.cmd.redraw() - local title = M.get_title(winid) + local title = get_title() local width = math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(title)) local title_winid = winid_map[winid] local bufnr @@ -476,7 +403,7 @@ M.add_title_to_win = function(winid, opts) if vim.api.nvim_win_get_buf(winid) ~= winbuf then return end - local new_title = M.get_title(winid) + local new_title = get_title() local new_width = math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(new_title)) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { " " .. new_title .. " " }) @@ -523,22 +450,18 @@ end ---@param action oil.Action ---@return oil.Adapter ----@return nil|oil.CrossAdapterAction M.get_adapter_for_action = function(action) - local adapter = assert(config.get_adapter_by_scheme(action.url or action.src_url)) + local adapter = config.get_adapter_by_scheme(action.url or action.src_url) + if not adapter then + error("no adapter found") + end if action.dest_url then - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) + local dest_adapter = config.get_adapter_by_scheme(action.dest_url) if adapter ~= dest_adapter then - if - adapter.supported_cross_adapter_actions - and adapter.supported_cross_adapter_actions[dest_adapter.name] - then - return adapter, adapter.supported_cross_adapter_actions[dest_adapter.name] - elseif - dest_adapter.supported_cross_adapter_actions - and dest_adapter.supported_cross_adapter_actions[adapter.name] - then - return dest_adapter, dest_adapter.supported_cross_adapter_actions[adapter.name] + if adapter.supports_xfer and adapter.supports_xfer[dest_adapter.name] then + return adapter + elseif dest_adapter.supports_xfer and dest_adapter.supports_xfer[adapter.name] then + return dest_adapter else error( string.format( @@ -646,7 +569,11 @@ M.render_text = function(bufnr, text, opts) pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines) vim.bo[bufnr].modifiable = false vim.bo[bufnr].modified = false - M.set_highlights(bufnr, highlights) + local ns = vim.api.nvim_create_namespace("Oil") + vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) + for _, hl in ipairs(highlights) do + vim.api.nvim_buf_add_highlight(bufnr, ns, unpack(hl)) + end end ---Run a function in the context of a full-editor window @@ -674,12 +601,8 @@ end ---@param bufnr integer ---@return boolean M.is_oil_bufnr = function(bufnr) - local filetype = vim.bo[bufnr].filetype - if filetype == "oil" then + if vim.bo[bufnr].filetype == "oil" then return true - elseif filetype ~= "" then - -- If the filetype is set and is NOT "oil", then it's not an oil buffer - return false end local scheme = M.parse_url(vim.api.nvim_buf_get_name(bufnr)) return config.adapters[scheme] or config.adapter_aliases[scheme] @@ -702,36 +625,15 @@ M.hack_around_termopen_autocmd = function(prev_mode) end, 10) end ----@param opts? {include_not_owned?: boolean} ---@return nil|integer -M.get_preview_win = function(opts) - opts = opts or {} - +M.get_preview_win = function() for _, winid in ipairs(vim.api.nvim_tabpage_list_wins(0)) do - if - vim.api.nvim_win_is_valid(winid) - and vim.wo[winid].previewwindow - and (opts.include_not_owned or vim.w[winid]["oil_preview"]) - then + if vim.api.nvim_win_is_valid(winid) and vim.wo[winid].previewwindow then return winid end end end ----@return fun() restore Function that restores the cursor -M.hide_cursor = function() - vim.api.nvim_set_hl(0, "OilPreviewCursor", { nocombine = true, blend = 100 }) - local original_guicursor = vim.go.guicursor - vim.go.guicursor = "a:OilPreviewCursor/OilPreviewCursor" - - return function() - -- HACK: see https://github.com/neovim/neovim/issues/21018 - vim.go.guicursor = "a:" - vim.cmd.redrawstatus() - vim.go.guicursor = original_guicursor - end -end - ---@param bufnr integer ---@param preferred_win nil|integer ---@return nil|integer @@ -756,283 +658,4 @@ M.buf_get_win = function(bufnr, preferred_win) return nil end ----@param adapter oil.Adapter ----@param url string ----@param opts {columns?: string[], no_cache?: boolean} ----@param callback fun(err: nil|string, entries: nil|oil.InternalEntry[]) -M.adapter_list_all = function(adapter, url, opts, callback) - local cache = require("oil.cache") - if not opts.no_cache then - local entries = cache.list_url(url) - if not vim.tbl_isempty(entries) then - return callback(nil, vim.tbl_values(entries)) - end - end - local ret = {} - adapter.list(url, opts.columns or {}, function(err, entries, fetch_more) - if err then - callback(err) - return - end - if entries then - vim.list_extend(ret, entries) - end - if fetch_more then - vim.defer_fn(fetch_more, 4) - else - callback(nil, ret) - end - end) -end - ----Send files from the current oil directory to quickfix ----based on the provided options. ----@param opts {target?: "qflist"|"loclist", action?: "r"|"a", only_matching_search?: boolean} -M.send_to_quickfix = function(opts) - if type(opts) ~= "table" then - opts = {} - end - local oil = require("oil") - local dir = oil.get_current_dir() - if type(dir) ~= "string" then - return - end - local range = M.get_visual_range() - if not range then - range = { start_lnum = 1, end_lnum = vim.fn.line("$") } - end - local match_all = not opts.only_matching_search - local qf_entries = {} - for i = range.start_lnum, range.end_lnum do - local entry = oil.get_entry_on_line(0, i) - if entry and entry.type == "file" and (match_all or M.is_matching(entry)) then - local qf_entry = { - filename = dir .. entry.name, - lnum = 1, - col = 1, - text = entry.name, - } - table.insert(qf_entries, qf_entry) - end - end - if #qf_entries == 0 then - vim.notify("[oil] No entries found to send to quickfix", vim.log.levels.WARN) - return - end - vim.api.nvim_exec_autocmds("QuickFixCmdPre", {}) - local qf_title = "oil files" - local action = opts.action == "a" and "a" or "r" - if opts.target == "loclist" then - vim.fn.setloclist(0, {}, action, { title = qf_title, items = qf_entries }) - vim.cmd.lopen() - else - vim.fn.setqflist({}, action, { title = qf_title, items = qf_entries }) - vim.cmd.copen() - end - vim.api.nvim_exec_autocmds("QuickFixCmdPost", {}) -end - ----@return boolean -M.is_visual_mode = function() - local mode = vim.api.nvim_get_mode().mode - return mode:match("^[vV]") ~= nil -end - ----Get the current visual selection range. If not in visual mode, return nil. ----@return {start_lnum: integer, end_lnum: integer}? -M.get_visual_range = function() - if not M.is_visual_mode() then - return - end - -- This is the best way to get the visual selection at the moment - -- https://github.com/neovim/neovim/pull/13896 - local _, start_lnum, _, _ = unpack(vim.fn.getpos("v")) - local _, end_lnum, _, _, _ = unpack(vim.fn.getcurpos()) - if start_lnum > end_lnum then - start_lnum, end_lnum = end_lnum, start_lnum - end - return { start_lnum = start_lnum, end_lnum = end_lnum } -end - ----@param entry oil.Entry ----@return boolean -M.is_matching = function(entry) - -- if search highlightig is not enabled, all files are considered to match - local search_highlighting_is_off = (vim.v.hlsearch == 0) - if search_highlighting_is_off then - return true - end - local pattern = vim.fn.getreg("/") - local position_of_match = vim.fn.match(entry.name, pattern) - return position_of_match ~= -1 -end - ----@param bufnr integer ----@param callback fun() -M.run_after_load = function(bufnr, callback) - if bufnr == 0 then - bufnr = vim.api.nvim_get_current_buf() - end - if vim.b[bufnr].oil_ready then - callback() - else - vim.api.nvim_create_autocmd("User", { - pattern = "OilEnter", - callback = function(args) - if args.data.buf == bufnr then - vim.api.nvim_buf_call(bufnr, callback) - return true - end - end, - }) - end -end - ----@param entry oil.Entry ----@return boolean -M.is_directory = function(entry) - local is_directory = entry.type == "directory" - or ( - entry.type == "link" - and entry.meta - and entry.meta.link_stat - and entry.meta.link_stat.type == "directory" - ) - return is_directory == true -end - ----Get the :edit path for an entry ----@param bufnr integer The oil buffer that contains the entry ----@param entry oil.Entry ----@param callback fun(normalized_url: string) -M.get_edit_path = function(bufnr, entry, callback) - local pathutil = require("oil.pathutil") - - local bufname = vim.api.nvim_buf_get_name(bufnr) - local scheme, dir = M.parse_url(bufname) - local adapter = M.get_adapter(bufnr, true) - assert(scheme and dir and adapter) - - local url = scheme .. dir .. entry.name - if M.is_directory(entry) then - url = url .. "/" - end - - if entry.name == ".." then - callback(scheme .. pathutil.parent(dir)) - elseif adapter.get_entry_path then - adapter.get_entry_path(url, entry, callback) - else - adapter.normalize_url(url, callback) - end -end - ---- Check for an icon provider and return a common icon provider API ----@return (oil.IconProvider)? -M.get_icon_provider = function() - -- prefer mini.icons - local _, mini_icons = pcall(require, "mini.icons") - ---@diagnostic disable-next-line: undefined-field - if _G.MiniIcons then -- `_G.MiniIcons` is a better check to see if the module is setup - return function(type, name) - return mini_icons.get(type == "directory" and "directory" or "file", name) - end - end - - -- fallback to `nvim-web-devicons` - local has_devicons, devicons = pcall(require, "nvim-web-devicons") - if has_devicons then - return function(type, name, conf) - if type == "directory" then - return conf and conf.directory or "", "OilDirIcon" - else - local icon, hl = devicons.get_icon(name) - icon = icon or (conf and conf.default_file or "") - return icon, hl - end - end - end -end - ----Read a buffer into a scratch buffer and apply syntactic highlighting when possible ----@param path string The path to the file to read ----@param preview_method oil.PreviewMethod ----@return nil|integer -M.read_file_to_scratch_buffer = function(path, preview_method) - local bufnr = vim.api.nvim_create_buf(false, true) - if bufnr == 0 then - return - end - - vim.bo[bufnr].bufhidden = "wipe" - vim.bo[bufnr].buftype = "nofile" - - local has_lines, read_res - if preview_method == "fast_scratch" then - has_lines, read_res = pcall(vim.fn.readfile, path, "", vim.o.lines) - else - has_lines, read_res = pcall(vim.fn.readfile, path) - end - local lines = has_lines and vim.split(table.concat(read_res, "\n"), "\n") or {} - - local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines) - if not ok then - return - end - local ft = vim.filetype.match({ filename = path, buf = bufnr }) - if ft and ft ~= "" and vim.treesitter.language.get_lang then - local lang = vim.treesitter.language.get_lang(ft) - if not pcall(vim.treesitter.start, bufnr, lang) then - vim.bo[bufnr].syntax = ft - else - end - end - - -- Replace the scratch buffer with a real buffer if we enter it - vim.api.nvim_create_autocmd("BufEnter", { - desc = "oil.nvim replace scratch buffer with real buffer", - buffer = bufnr, - callback = function() - local winid = vim.api.nvim_get_current_win() - -- Have to schedule this so all the FileType, etc autocmds will fire - vim.schedule(function() - if vim.api.nvim_get_current_win() == winid then - vim.cmd.edit({ args = { path } }) - - -- If we're still in a preview window, make sure this buffer still gets treated as a - -- preview - if vim.wo.previewwindow then - vim.bo.bufhidden = "wipe" - vim.b.oil_preview_buffer = true - end - end - end) - end, - }) - - return bufnr -end - -local _regcache = {} ----Check if a file matches a BufReadCmd autocmd ----@param filename string ----@return boolean -M.file_matches_bufreadcmd = function(filename) - local autocmds = vim.api.nvim_get_autocmds({ - event = "BufReadCmd", - }) - for _, au in ipairs(autocmds) do - local pat = _regcache[au.pattern] - if not pat then - pat = vim.fn.glob2regpat(au.pattern) - _regcache[au.pattern] = pat - end - - if vim.fn.match(filename, pat) >= 0 then - return true - end - end - return false -end - return M diff --git a/lua/oil/view.lua b/lua/oil/view.lua index b3a216e..e92587a 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -1,9 +1,7 @@ -local uv = vim.uv or vim.loop local cache = require("oil.cache") local columns = require("oil.columns") local config = require("oil.config") local constants = require("oil.constants") -local fs = require("oil.fs") local keymap_util = require("oil.keymap_util") local loading = require("oil.loading") local util = require("oil.util") @@ -17,18 +15,13 @@ local FIELD_META = constants.FIELD_META -- map of path->last entry under cursor local last_cursor_entry = {} ----@param name string +---@param entry oil.InternalEntry ---@param bufnr integer ----@return boolean display ----@return boolean is_hidden Whether the file is classified as a hidden file -M.should_display = function(name, bufnr) - if config.view_options.is_always_hidden(name, bufnr) then - return false, true - else - local is_hidden = config.view_options.is_hidden_file(name, bufnr) - local display = config.view_options.show_hidden or not is_hidden - return display, is_hidden - end +---@return boolean +M.should_display = function(entry, bufnr) + local name = entry[FIELD_NAME] + return not config.view_options.is_always_hidden(name, bufnr) + and (not config.view_options.is_hidden_file(name, bufnr) or config.view_options.show_hidden) end ---@param bufname string @@ -85,7 +78,7 @@ M.toggle_hidden = function() end end ----@param is_hidden_file fun(filename: string, bufnr: integer): boolean +---@param is_hidden_file fun(filename: string, bufnr: nil|integer): boolean M.set_is_hidden_file = function(is_hidden_file) local any_modified = are_any_modified() if any_modified then @@ -107,22 +100,7 @@ M.set_columns = function(cols) end end -M.set_sort = function(new_sort) - local any_modified = are_any_modified() - if any_modified then - vim.notify("Cannot change sorting when you have unsaved changes", vim.log.levels.WARN) - else - config.view_options.sort = new_sort - -- TODO only refetch if we don't have all the necessary data for the columns - M.rerender_all_oil_buffers({ refetch = true }) - end -end - ----@class oil.ViewData ----@field fs_event? any uv_fs_event_t - -- List of bufnrs ----@type table local session = {} ---@return integer[] @@ -146,7 +124,7 @@ M.unlock_buffers = function() buffers_locked = false for bufnr in pairs(session) do if vim.api.nvim_buf_is_loaded(bufnr) then - local adapter = util.get_adapter(bufnr, true) + local adapter = util.get_adapter(bufnr) if adapter then vim.bo[bufnr].modifiable = adapter.is_modifiable(bufnr) end @@ -154,12 +132,10 @@ M.unlock_buffers = function() end end ----@param opts? table ----@param callback? fun(err: nil|string) +---@param opts table ---@note --- This DISCARDS ALL MODIFICATIONS a user has made to oil buffers -M.rerender_all_oil_buffers = function(opts, callback) - opts = opts or {} +M.rerender_all_oil_buffers = function(opts) local buffers = M.get_all_buffers() local hidden_buffers = {} for _, bufnr in ipairs(buffers) do @@ -170,31 +146,38 @@ M.rerender_all_oil_buffers = function(opts, callback) hidden_buffers[vim.api.nvim_win_get_buf(winid)] = nil end end - local cb = util.cb_collect(#buffers, callback or function() end) for _, bufnr in ipairs(buffers) do if hidden_buffers[bufnr] then vim.b[bufnr].oil_dirty = opts -- We also need to mark this as nomodified so it doesn't interfere with quitting vim vim.bo[bufnr].modified = false - vim.schedule(cb) else - M.render_buffer_async(bufnr, opts, cb) + M.render_buffer_async(bufnr, opts) end end end M.set_win_options = function() local winid = vim.api.nvim_get_current_win() - - -- work around https://github.com/neovim/neovim/pull/27422 - vim.api.nvim_set_option_value("foldmethod", "manual", { scope = "local", win = winid }) - for k, v in pairs(config.win_options) do + if config.restore_win_options then + local varname = "_oil_" .. k + if not pcall(vim.api.nvim_win_get_var, winid, varname) then + local prev_value = vim.wo[k] + vim.api.nvim_win_set_var(winid, varname, prev_value) + end + end vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) end - if vim.wo[winid].previewwindow then -- apply preview window options last - for k, v in pairs(config.preview_win.win_options) do - vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) +end + +M.restore_win_options = function() + local winid = vim.api.nvim_get_current_win() + for k in pairs(config.win_options) do + local varname = "_oil_" .. k + local has_opt, opt = pcall(vim.api.nvim_win_get_var, winid, varname) + if has_opt then + vim.api.nvim_set_option_value(k, opt, { scope = "local", win = winid }) end end end @@ -202,8 +185,8 @@ end ---Get a list of visible oil buffers and a list of hidden oil buffers ---@note --- If any buffers are modified, return values are nil ----@return nil|integer[] visible ----@return nil|integer[] hidden +---@return nil|integer[] +---@return nil|integer[] local function get_visible_hidden_buffers() local buffers = M.get_all_buffers() local hidden_buffers = {} @@ -227,12 +210,7 @@ end ---Delete unmodified, hidden oil buffers and if none remain, clear the cache M.delete_hidden_buffers = function() local visible_buffers, hidden_buffers = get_visible_hidden_buffers() - if - not visible_buffers - or not hidden_buffers - or not vim.tbl_isempty(visible_buffers) - or vim.fn.win_gettype() == "command" - then + if not visible_buffers or not hidden_buffers or not vim.tbl_isempty(visible_buffers) then return end for _, bufnr in ipairs(hidden_buffers) do @@ -241,122 +219,6 @@ M.delete_hidden_buffers = function() cache.clear_everything() end ----@param adapter oil.Adapter ----@param ranges table ----@return integer -local function get_first_mutable_column_col(adapter, ranges) - local min_col = ranges.name[1] - for col_name, start_len in pairs(ranges) do - local start = start_len[1] - local col_spec = columns.get_column(adapter, col_name) - local is_col_mutable = col_spec and col_spec.perform_action ~= nil - if is_col_mutable and start < min_col then - min_col = start - end - end - return min_col -end - ---- @param bufnr integer ---- @param adapter oil.Adapter ---- @param mode false|"name"|"editable" ---- @param cur integer[] ---- @return integer[] | nil -local function calc_constrained_cursor_pos(bufnr, adapter, mode, cur) - local parser = require("oil.mutator.parser") - local line = vim.api.nvim_buf_get_lines(bufnr, cur[1] - 1, cur[1], true)[1] - local column_defs = columns.get_supported_columns(adapter) - local result = parser.parse_line(adapter, line, column_defs) - if result and result.ranges then - local min_col - if mode == "editable" then - min_col = get_first_mutable_column_col(adapter, result.ranges) - elseif mode == "name" then - min_col = result.ranges.name[1] - else - error(string.format('Unexpected value "%s" for option constrain_cursor', mode)) - end - if cur[2] < min_col then - return { cur[1], min_col } - end - end -end - ----Force cursor to be after hidden/immutable columns ----@param bufnr integer ----@param mode false|"name"|"editable" -local function constrain_cursor(bufnr, mode) - if not mode then - return - end - if bufnr ~= vim.api.nvim_get_current_buf() then - return - end - - local adapter = util.get_adapter(bufnr, true) - if not adapter then - return - end - - local mc = package.loaded["multicursor-nvim"] - if mc then - mc.onSafeState(function() - mc.action(function(ctx) - ctx:forEachCursor(function(cursor) - local new_cur = - calc_constrained_cursor_pos(bufnr, adapter, mode, { cursor:line(), cursor:col() - 1 }) - if new_cur then - cursor:setPos({ new_cur[1], new_cur[2] + 1 }) - end - end) - end) - end, { once = true }) - else - local cur = vim.api.nvim_win_get_cursor(0) - local new_cur = calc_constrained_cursor_pos(bufnr, adapter, mode, cur) - if new_cur then - vim.api.nvim_win_set_cursor(0, new_cur) - end - end -end - ----Redraw original path virtual text for trash buffer ----@param bufnr integer -local function redraw_trash_virtual_text(bufnr) - if not vim.api.nvim_buf_is_valid(bufnr) or not vim.api.nvim_buf_is_loaded(bufnr) then - return - end - local parser = require("oil.mutator.parser") - local adapter = util.get_adapter(bufnr, true) - if not adapter or adapter.name ~= "trash" then - return - end - local _, buf_path = util.parse_url(vim.api.nvim_buf_get_name(bufnr)) - local os_path = fs.posix_to_os_path(assert(buf_path)) - local ns = vim.api.nvim_create_namespace("OilVtext") - vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) - local column_defs = columns.get_supported_columns(adapter) - for lnum, line in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)) do - local result = parser.parse_line(adapter, line, column_defs) - local entry = result and result.entry - if entry then - local meta = entry[FIELD_META] - ---@type nil|oil.TrashInfo - local trash_info = meta and meta.trash_info - if trash_info then - vim.api.nvim_buf_set_extmark(bufnr, ns, lnum - 1, 0, { - virt_text = { - { - "➜ " .. fs.shorten_path(trash_info.original_path, os_path), - "OilTrashSourcePath", - }, - }, - }) - end - end - end -end - ---@param bufnr integer M.initialize = function(bufnr) if bufnr == 0 then @@ -370,52 +232,39 @@ M.initialize = function(bufnr) group = "Oil", }) vim.bo[bufnr].buftype = "acwrite" - vim.bo[bufnr].swapfile = false vim.bo[bufnr].syntax = "oil" vim.bo[bufnr].filetype = "oil" vim.b[bufnr].EditorConfig_disable = 1 - session[bufnr] = session[bufnr] or {} + session[bufnr] = true for k, v in pairs(config.buf_options) do - vim.bo[bufnr][k] = v + vim.api.nvim_buf_set_option(bufnr, k, v) end - vim.api.nvim_buf_call(bufnr, M.set_win_options) - + M.set_win_options() vim.api.nvim_create_autocmd("BufHidden", { desc = "Delete oil buffers when no longer in use", group = "Oil", nested = true, buffer = bufnr, callback = function() - -- First wait a short time (100ms) for the buffer change to settle + -- First wait a short time (10ms) for the buffer change to settle vim.defer_fn(function() local visible_buffers = get_visible_hidden_buffers() - -- Only delete oil buffers if none of them are visible + -- Only kick off the 2-second timer if we don't have any visible oil buffers if visible_buffers and vim.tbl_isempty(visible_buffers) then - -- Check if cleanup is enabled - if type(config.cleanup_delay_ms) == "number" then - if config.cleanup_delay_ms > 0 then - vim.defer_fn(function() - M.delete_hidden_buffers() - end, config.cleanup_delay_ms) - else - M.delete_hidden_buffers() - end - end + vim.defer_fn(function() + M.delete_hidden_buffers() + end, 2000) end - end, 100) + end, 10) end, }) - vim.api.nvim_create_autocmd("BufUnload", { + vim.api.nvim_create_autocmd("BufDelete", { group = "Oil", nested = true, once = true, buffer = bufnr, callback = function() - local view_data = session[bufnr] session[bufnr] = nil - if view_data and view_data.fs_event then - view_data.fs_event:stop() - end end, }) vim.api.nvim_create_autocmd("BufEnter", { @@ -430,135 +279,62 @@ M.initialize = function(bufnr) end, }) local timer - vim.api.nvim_create_autocmd("InsertEnter", { - desc = "Constrain oil cursor position", - group = "Oil", - buffer = bufnr, - callback = function() - -- For some reason the cursor bounces back to its original position, - -- so we have to defer the call - vim.schedule_wrap(constrain_cursor)(bufnr, config.constrain_cursor) - end, - }) - vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, { + vim.api.nvim_create_autocmd("CursorMoved", { desc = "Update oil preview window", group = "Oil", buffer = bufnr, callback = function() local oil = require("oil") + local parser = require("oil.mutator.parser") if vim.wo.previewwindow then return end - constrain_cursor(bufnr, config.constrain_cursor) - - if config.preview_win.update_on_cursor_moved then - -- Debounce and update the preview window - if timer then - timer:again() - return - end - timer = uv.new_timer() - if not timer then - return - end - timer:start(10, 100, function() - timer:stop() - timer:close() - timer = nil - vim.schedule(function() - if vim.api.nvim_get_current_buf() ~= bufnr then - return - end - local entry = oil.get_cursor_entry() - -- Don't update in visual mode. Visual mode implies editing not browsing, - -- and updating the preview can cause flicker and stutter. - if entry and not util.is_visual_mode() then - local winid = util.get_preview_win() - if winid then - if entry.id ~= vim.w[winid].oil_entry_id then - oil.open_preview() - end - end - end - end) - end) - end - end, - }) - - local adapter = util.get_adapter(bufnr, true) - - -- Set up a watcher that will refresh the directory - if - adapter - and adapter.name == "files" - and config.watch_for_changes - and not session[bufnr].fs_event - then - local fs_event = assert(uv.new_fs_event()) - local bufname = vim.api.nvim_buf_get_name(bufnr) - local _, dir = util.parse_url(bufname) - fs_event:start( - assert(dir), - {}, - vim.schedule_wrap(function(err, filename, events) - if not vim.api.nvim_buf_is_valid(bufnr) then - local sess = session[bufnr] - if sess then - sess.fs_event = nil + -- Force the cursor to be after the (concealed) ID at the beginning of the line + local adapter = util.get_adapter(bufnr) + if adapter then + local cur = vim.api.nvim_win_get_cursor(0) + local line = vim.api.nvim_buf_get_lines(bufnr, cur[1] - 1, cur[1], true)[1] + local column_defs = columns.get_supported_columns(adapter) + local result = parser.parse_line(adapter, line, column_defs) + if result and result.data then + local min_col = result.ranges.id[2] + 1 + if cur[2] < min_col then + vim.api.nvim_win_set_cursor(0, { cur[1], min_col }) end - fs_event:stop() - return - end - local mutator = require("oil.mutator") - if err or vim.bo[bufnr].modified or vim.b[bufnr].oil_dirty or mutator.is_mutating() then - return end + end - -- If the buffer is currently visible, rerender - for _, winid in ipairs(vim.api.nvim_list_wins()) do - if vim.api.nvim_win_is_valid(winid) and vim.api.nvim_win_get_buf(winid) == bufnr then - M.render_buffer_async(bufnr) + -- Debounce and update the preview window + if timer then + timer:again() + return + end + timer = vim.loop.new_timer() + if not timer then + return + end + timer:start(10, 100, function() + timer:stop() + timer:close() + timer = nil + vim.schedule(function() + if vim.api.nvim_get_current_buf() ~= bufnr then return end - end - - -- If it is not currently visible, mark it as dirty - vim.b[bufnr].oil_dirty = {} - end) - ) - session[bufnr].fs_event = fs_event - end - - -- Watch for TextChanged and update the trash original path extmarks - if adapter and adapter.name == "trash" then - local debounce_timer = assert(uv.new_timer()) - local pending = false - vim.api.nvim_create_autocmd("TextChanged", { - desc = "Update oil virtual text of original path", - buffer = bufnr, - callback = function() - -- Respond immediately to prevent flickering, the set the timer for a "cooldown period" - -- If this is called again during the cooldown window, we will rerender after cooldown. - if debounce_timer:is_active() then - pending = true - else - redraw_trash_virtual_text(bufnr) - end - debounce_timer:start( - 50, - 0, - vim.schedule_wrap(function() - if pending then - pending = false - redraw_trash_virtual_text(bufnr) + local entry = oil.get_cursor_entry() + if entry then + local winid = util.get_preview_win() + if winid then + if entry.id ~= vim.w[winid].oil_entry_id then + oil.select({ preview = true }) + end end - end) - ) - end, - }) - end + end + end) + end) + end, + }) M.render_buffer_async(bufnr, {}, function(err) if err then vim.notify( @@ -566,66 +342,26 @@ M.initialize = function(bufnr) vim.log.levels.ERROR ) else - vim.b[bufnr].oil_ready = true vim.api.nvim_exec_autocmds( "User", { pattern = "OilEnter", modeline = false, data = { buf = bufnr } } ) end end) - keymap_util.set_keymaps(config.keymaps, bufnr) + keymap_util.set_keymaps("", config.keymaps, bufnr) end ----@param adapter oil.Adapter ----@param num_entries integer ----@return fun(a: oil.InternalEntry, b: oil.InternalEntry): boolean -local function get_sort_function(adapter, num_entries) - local idx_funs = {} - local sort_config = config.view_options.sort - - -- If empty, default to type + name sorting - if vim.tbl_isempty(sort_config) then - sort_config = { { "type", "asc" }, { "name", "asc" } } - end - - for _, sort_pair in ipairs(sort_config) do - local col_name, order = unpack(sort_pair) - if order ~= "asc" and order ~= "desc" then - vim.notify_once( - string.format( - "Column '%s' has invalid sort order '%s'. Should be either 'asc' or 'desc'", - col_name, - order - ), - vim.log.levels.WARN - ) - end - local col = columns.get_column(adapter, col_name) - if col and col.create_sort_value_factory then - table.insert(idx_funs, { col.create_sort_value_factory(num_entries), order }) - elseif col and col.get_sort_value then - table.insert(idx_funs, { col.get_sort_value, order }) - else - vim.notify_once( - string.format("Column '%s' does not support sorting", col_name), - vim.log.levels.WARN - ) - end - end - return function(a, b) - for _, sort_fn in ipairs(idx_funs) do - local get_sort_value, order = sort_fn[1], sort_fn[2] - local a_val = get_sort_value(a) - local b_val = get_sort_value(b) - if a_val ~= b_val then - if order == "desc" then - return a_val > b_val - else - return a_val < b_val - end - end - end - return a[FIELD_NAME] < b[FIELD_NAME] +---@param entry oil.InternalEntry +---@return boolean +local function is_entry_directory(entry) + local type = entry[FIELD_TYPE] + if type == "directory" then + return true + elseif type == "link" then + local meta = entry[FIELD_META] + return meta and meta.link_stat and meta.link_stat.type == "directory" + else + return false end end @@ -647,17 +383,21 @@ local function render_buffer(bufnr, opts) jump_first = false, }) local scheme = util.parse_url(bufname) - local adapter = util.get_adapter(bufnr, true) + local adapter = util.get_adapter(bufnr) if not scheme or not adapter then return false end local entries = cache.list_url(bufname) local entry_list = vim.tbl_values(entries) - -- Only sort the entries once we have them all - if not vim.b[bufnr].oil_rendering then - table.sort(entry_list, get_sort_function(adapter, #entry_list)) - end + table.sort(entry_list, function(a, b) + local a_isdir = is_entry_directory(a) + local b_isdir = is_entry_directory(b) + if a_isdir ~= b_isdir then + return a_isdir + end + return a[FIELD_NAME] < b[FIELD_NAME] + end) local jump_idx if opts.jump_first then @@ -668,63 +408,50 @@ local function render_buffer(bufnr, opts) local column_defs = columns.get_supported_columns(scheme) local line_table = {} local col_width = {} - local col_align = {} - for i, col_def in ipairs(column_defs) do + for i in ipairs(column_defs) do col_width[i + 1] = 1 - local _, conf = util.split_config(col_def) - col_align[i + 1] = conf and conf.align or "left" end - - if M.should_display("..", bufnr) then - local cols = - M.format_entry_cols({ 0, "..", "directory" }, column_defs, col_width, adapter, true, bufnr) - table.insert(line_table, cols) - end - for _, entry in ipairs(entry_list) do - local should_display, is_hidden = M.should_display(entry[FIELD_NAME], bufnr) - if should_display then - local cols = M.format_entry_cols(entry, column_defs, col_width, adapter, is_hidden, bufnr) - table.insert(line_table, cols) - - local name = entry[FIELD_NAME] - if seek_after_render == name then - seek_after_render_found = true - jump_idx = #line_table - end + if not M.should_display(entry, bufnr) then + goto continue end + local cols = M.format_entry_cols(entry, column_defs, col_width, adapter) + table.insert(line_table, cols) + + local name = entry[FIELD_NAME] + if seek_after_render == name then + seek_after_render_found = true + jump_idx = #line_table + M.set_last_cursor(bufname, nil) + end + ::continue:: end - local lines, highlights = util.render_table(line_table, col_width, col_align) + local lines, highlights = util.render_table(line_table, col_width) vim.bo[bufnr].modifiable = true vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) vim.bo[bufnr].modifiable = false vim.bo[bufnr].modified = false util.set_highlights(bufnr, highlights) - if opts.jump then -- TODO why is the schedule necessary? vim.schedule(function() for _, winid in ipairs(vim.api.nvim_list_wins()) do if vim.api.nvim_win_is_valid(winid) and vim.api.nvim_win_get_buf(winid) == bufnr then - if jump_idx then - local lnum = jump_idx - local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1] - local id_str = line:match("^/(%d+)") - local id = tonumber(id_str) - if id then - local entry = cache.get_entry_by_id(id) - if entry then - local name = entry[FIELD_NAME] - local col = line:find(name, 1, true) or (id_str:len() + 1) - vim.api.nvim_win_set_cursor(winid, { lnum, col - 1 }) - return - end + -- If we're not jumping to a specific lnum, use the current lnum so we can adjust the col + local lnum = jump_idx or vim.api.nvim_win_get_cursor(winid)[1] + local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1] + local id_str = line:match("^/(%d+)") + local id = tonumber(id_str) + if id then + local entry = cache.get_entry_by_id(id) + if entry then + local name = entry[FIELD_NAME] + local col = line:find(name, 1, true) or (id_str:len() + 1) + vim.api.nvim_win_set_cursor(winid, { lnum, col - 1 }) end end - - constrain_cursor(bufnr, "name") end end end) @@ -732,48 +459,14 @@ local function render_buffer(bufnr, opts) return seek_after_render_found end ----@param name string ----@param meta? table ----@return string filename ----@return string|nil link_target -local function get_link_text(name, meta) - local link_text - if meta then - if meta.link_stat and meta.link_stat.type == "directory" then - name = name .. "/" - end - - if meta.link then - link_text = "-> " .. meta.link - if meta.link_stat and meta.link_stat.type == "directory" then - link_text = util.addslash(link_text) - end - end - end - - return name, link_text -end - ---@private ---@param entry oil.InternalEntry ---@param column_defs table[] ---@param col_width integer[] ---@param adapter oil.Adapter ----@param is_hidden boolean ----@param bufnr integer ---@return oil.TextChunk[] -M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden, bufnr) +M.format_entry_cols = function(entry, column_defs, col_width, adapter) local name = entry[FIELD_NAME] - local meta = entry[FIELD_META] - local hl_suffix = "" - if is_hidden then - hl_suffix = "Hidden" - end - if meta and meta.display_name then - name = meta.display_name - end - -- We can't handle newlines in filenames (and shame on you for doing that) - name = name:gsub("\n", "") -- First put the unique ID local cols = {} local id_key = cache.format_id(entry[FIELD_ID]) @@ -781,126 +474,69 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden table.insert(cols, id_key) -- Then add all the configured columns for i, column in ipairs(column_defs) do - local chunk = columns.render_col(adapter, column, entry, bufnr) + local chunk = columns.render_col(adapter, column, entry) local text = type(chunk) == "table" and chunk[1] or chunk - ---@cast text string col_width[i + 1] = math.max(col_width[i + 1], vim.api.nvim_strwidth(text)) table.insert(cols, chunk) end -- Always add the entry name at the end local entry_type = entry[FIELD_TYPE] - - local get_custom_hl = config.view_options.highlight_filename - local link_name, link_name_hl, link_target, link_target_hl - if get_custom_hl then - local external_entry = util.export_entry(entry) - - if entry_type == "link" then - link_name, link_target = get_link_text(name, meta) - local is_orphan = not (meta and meta.link_stat) - link_name_hl = get_custom_hl(external_entry, is_hidden, false, is_orphan, bufnr) - - if link_target then - link_target_hl = get_custom_hl(external_entry, is_hidden, true, is_orphan, bufnr) - end - - -- intentional fallthrough - else - local hl = get_custom_hl(external_entry, is_hidden, false, false, bufnr) - if hl then - -- Add the trailing / if this is a directory, this is important - if entry_type == "directory" then - name = name .. "/" - end - table.insert(cols, { name, hl }) - return cols - end - end - end - if entry_type == "directory" then - table.insert(cols, { name .. "/", "OilDir" .. hl_suffix }) + table.insert(cols, { name .. "/", "OilDir" }) elseif entry_type == "socket" then - table.insert(cols, { name, "OilSocket" .. hl_suffix }) + table.insert(cols, { name, "OilSocket" }) elseif entry_type == "link" then - if not link_name then - link_name, link_target = get_link_text(name, meta) - end - local is_orphan = not (meta and meta.link_stat) - if not link_name_hl then - link_name_hl = (is_orphan and "OilOrphanLink" or "OilLink") .. hl_suffix - end - table.insert(cols, { link_name, link_name_hl }) - - if link_target then - if not link_target_hl then - link_target_hl = (is_orphan and "OilOrphanLinkTarget" or "OilLinkTarget") .. hl_suffix + local meta = entry[FIELD_META] + local link_text + if meta then + if meta.link_stat and meta.link_stat.type == "directory" then + name = name .. "/" end - table.insert(cols, { link_target, link_target_hl }) + + if meta.link then + link_text = "->" .. " " .. meta.link + if meta.link_stat and meta.link_stat.type == "directory" then + link_text = util.addslash(link_text) + end + end + end + + table.insert(cols, { name, "OilLink" }) + if link_text then + table.insert(cols, { link_text, "Comment" }) end else - table.insert(cols, { name, "OilFile" .. hl_suffix }) - end - - return cols -end - ----Get the column names that are used for view and sort ----@return string[] -local function get_used_columns() - local cols = {} - for _, def in ipairs(config.columns) do - local name = util.split_config(def) - table.insert(cols, name) - end - for _, sort_pair in ipairs(config.view_options.sort) do - local name = sort_pair[1] - table.insert(cols, name) + table.insert(cols, { name, "OilFile" }) end return cols end ----@type table -local pending_renders = {} - ---@param bufnr integer ---@param opts nil|table +--- preserve_undo nil|boolean --- refetch nil|boolean Defaults to true ---@param callback nil|fun(err: nil|string) M.render_buffer_async = function(bufnr, opts, callback) opts = vim.tbl_deep_extend("keep", opts or {}, { + preserve_undo = false, refetch = true, }) if bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end - - -- If we're already rendering, queue up another rerender after it's complete - if vim.b[bufnr].oil_rendering then - if not pending_renders[bufnr] then - pending_renders[bufnr] = { callback } - elseif callback then - table.insert(pending_renders[bufnr], callback) - end - return - end - local bufname = vim.api.nvim_buf_get_name(bufnr) - vim.b[bufnr].oil_rendering = true - local _, dir = util.parse_url(bufname) - -- Undo should not return to a blank buffer - -- Method taken from :h clear-undo - vim.bo[bufnr].undolevels = -1 + local scheme, dir = util.parse_url(bufname) + local preserve_undo = opts.preserve_undo and config.adapters[scheme] == "files" + if not preserve_undo then + -- Undo should not return to a blank buffer + -- Method taken from :h clear-undo + vim.bo[bufnr].undolevels = -1 + end local handle_error = vim.schedule_wrap(function(message) - vim.b[bufnr].oil_rendering = false - vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" }) - util.render_text(bufnr, { "Error: " .. message }) - if pending_renders[bufnr] then - for _, cb in ipairs(pending_renders[bufnr]) do - cb(message) - end - pending_renders[bufnr] = nil + if not preserve_undo then + vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" }) end + util.render_text(bufnr, { "Error: " .. message }) if callback then callback(message) else @@ -911,80 +547,54 @@ M.render_buffer_async = function(bufnr, opts, callback) handle_error(string.format("Could not parse oil url '%s'", bufname)) return end - local adapter = util.get_adapter(bufnr, true) + local adapter = util.get_adapter(bufnr) if not adapter then handle_error(string.format("[oil] no adapter for buffer '%s'", bufname)) return end - local start_ms = uv.hrtime() / 1e6 + local start_ms = vim.loop.hrtime() / 1e6 local seek_after_render_found = false local first = true vim.bo[bufnr].modifiable = false - vim.bo[bufnr].modified = false loading.set_loading(bufnr, true) local finish = vim.schedule_wrap(function() if not vim.api.nvim_buf_is_valid(bufnr) then return end - vim.b[bufnr].oil_rendering = false loading.set_loading(bufnr, false) render_buffer(bufnr, { jump = true }) - M.set_last_cursor(bufname, nil) - vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" }) + if not preserve_undo then + vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" }) + end vim.bo[bufnr].modifiable = not buffers_locked and adapter.is_modifiable(bufnr) if callback then callback() end - - -- If there were any concurrent calls to render this buffer, process them now - if pending_renders[bufnr] then - local all_cbs = pending_renders[bufnr] - pending_renders[bufnr] = nil - local new_cb = function(...) - for _, cb in ipairs(all_cbs) do - cb(...) - end - end - M.render_buffer_async(bufnr, {}, new_cb) - end end) if not opts.refetch then finish() return end - cache.begin_update_url(bufname) - local num_iterations = 0 - adapter.list(bufname, get_used_columns(), function(err, entries, fetch_more) + adapter.list(bufname, config.columns, function(err, has_more) loading.set_loading(bufnr, false) if err then - cache.end_update_url(bufname) handle_error(err) return - end - if entries then - for _, entry in ipairs(entries) do - cache.store_entry(bufname, entry) - end - end - if fetch_more then - local now = uv.hrtime() / 1e6 + elseif has_more then + local now = vim.loop.hrtime() / 1e6 local delta = now - start_ms -- If we've been chugging for more than 40ms, go ahead and render what we have - if (delta > 25 and num_iterations < 1) or delta > 500 then - num_iterations = num_iterations + 1 + if delta > 40 then start_ms = now vim.schedule(function() seek_after_render_found = render_buffer(bufnr, { jump = not seek_after_render_found, jump_first = first }) - start_ms = uv.hrtime() / 1e6 end) end first = false - vim.defer_fn(fetch_more, 4) else - cache.end_update_url(bufname) -- done iterating finish() end diff --git a/lua/resession/extensions/oil.lua b/lua/resession/extensions/oil.lua index 427843d..b3f8058 100644 --- a/lua/resession/extensions/oil.lua +++ b/lua/resession/extensions/oil.lua @@ -1,5 +1,11 @@ local M = {} +M.on_save = function() + return {} +end + +M.on_load = function(data) end + M.is_win_supported = function(winid, bufnr) return vim.bo[bufnr].filetype == "oil" end diff --git a/perf/bootstrap.lua b/perf/bootstrap.lua deleted file mode 100644 index 5f10c06..0000000 --- a/perf/bootstrap.lua +++ /dev/null @@ -1,63 +0,0 @@ -vim.opt.runtimepath:prepend("scripts/benchmark.nvim") -vim.opt.runtimepath:prepend(".") - -local bm = require("benchmark") -bm.sandbox() - ----@module 'oil' ----@type oil.SetupOpts -local setup_opts = { - -- columns = { "icon", "permissions", "size", "mtime" }, -} - -local DIR_SIZE = tonumber(vim.env.DIR_SIZE) or 100000 -local ITERATIONS = tonumber(vim.env.ITERATIONS) or 10 -local WARM_UP = tonumber(vim.env.WARM_UP) or 1 -local OUTLIERS = tonumber(vim.env.OUTLIERS) or math.floor(ITERATIONS / 10) -local TEST_DIR = "perf/tmp/test_" .. DIR_SIZE - -vim.fn.mkdir(TEST_DIR, "p") -require("benchmark.files").create_files(TEST_DIR, "file %d.txt", DIR_SIZE) - -function _G.jit_profile() - require("oil").setup(setup_opts) - local finish = bm.jit_profile({ filename = TEST_DIR .. "/profile.txt" }) - bm.wait_for_user_event("OilEnter", function() - finish() - end) - require("oil").open(TEST_DIR) -end - -function _G.flame_profile() - local start, stop = bm.flame_profile({ - pattern = "oil*", - filename = "profile.json", - }) - require("oil").setup(setup_opts) - start() - bm.wait_for_user_event("OilEnter", function() - stop(function() - vim.cmd.qall({ mods = { silent = true } }) - end) - end) - require("oil").open(TEST_DIR) -end - -function _G.benchmark() - require("oil").setup(setup_opts) - bm.run({ title = "oil.nvim", iterations = ITERATIONS, warm_up = WARM_UP }, function(callback) - bm.wait_for_user_event("OilEnter", callback) - require("oil").open(TEST_DIR) - end, function(times) - local avg = bm.avg(times, { trim_outliers = OUTLIERS }) - local std_dev = bm.std_dev(times, { trim_outliers = OUTLIERS }) - local lines = { - table.concat(vim.tbl_map(bm.format_time, times), " "), - string.format("Average: %s", bm.format_time(avg)), - string.format("Std deviation: %s", bm.format_time(std_dev)), - } - - vim.fn.writefile(lines, "perf/tmp/benchmark.txt") - vim.cmd.qall({ mods = { silent = true } }) - end) -end diff --git a/run_tests.sh b/run_tests.sh index 3018bc0..98b4fa7 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash set -e mkdir -p ".testenv/config/nvim" diff --git a/scripts/generate.py b/scripts/generate.py deleted file mode 100755 index 4e02550..0000000 --- a/scripts/generate.py +++ /dev/null @@ -1,407 +0,0 @@ -import os -import os.path -import re -from dataclasses import dataclass, field -from typing import Any, Dict, List - -from nvim_doc_tools import ( - LuaParam, - LuaTypes, - Vimdoc, - VimdocSection, - generate_md_toc, - indent, - leftright, - parse_directory, - read_nvim_json, - read_section, - render_md_api2, - render_vimdoc_api2, - replace_section, - wrap, -) -from nvim_doc_tools.vimdoc import format_vimdoc_params - -HERE = os.path.dirname(__file__) -ROOT = os.path.abspath(os.path.join(HERE, os.path.pardir)) -README = os.path.join(ROOT, "README.md") -DOC = os.path.join(ROOT, "doc") -VIMDOC = os.path.join(DOC, "oil.txt") - - -def add_md_link_path(path: str, lines: List[str]) -> List[str]: - ret = [] - for line in lines: - ret.append(re.sub(r"(\(#)", "(" + path + "#", line)) - return ret - - -def update_md_api(): - api_doc = os.path.join(DOC, "api.md") - types = parse_directory(os.path.join(ROOT, "lua")) - funcs = types.files["oil/init.lua"].functions - lines = ["\n"] + render_md_api2(funcs, types, 2) + ["\n"] - replace_section( - api_doc, - r"^$", - r"^$", - lines, - ) - toc = ["\n"] + generate_md_toc(api_doc, max_level=1) + ["\n"] - replace_section( - api_doc, - r"^$", - r"^$", - toc, - ) - toc = add_md_link_path("doc/api.md", toc) - replace_section( - README, - r"^$", - r"^$", - toc, - ) - - -def update_readme(): - def get_toc(filename: str) -> List[str]: - subtoc = generate_md_toc(os.path.join(DOC, filename)) - return add_md_link_path("doc/" + filename, subtoc) - - recipes_toc = get_toc("recipes.md") - - replace_section( - README, - r"^## Recipes$", - r"^#", - ["\n"] + recipes_toc + ["\n"], - ) - - -def update_md_toc(filename: str, max_level: int = 99): - toc = ["\n"] + generate_md_toc(filename, max_level) + ["\n"] - replace_section( - filename, - r"^$", - r"^$", - toc, - ) - - -def update_config_options(): - config_file = os.path.join(ROOT, "lua", "oil", "config.lua") - opt_lines = ['\n```lua\nrequire("oil").setup({\n'] - opt_lines.extend(read_section(config_file, r"^\s*local default_config =", r"^}$")) - replace_section( - README, - r"^## Options$", - r"^}\)$", - opt_lines, - ) - - -@dataclass -class ColumnDef: - name: str - adapters: str - editable: bool - sortable: bool - summary: str - params: List["LuaParam"] = field(default_factory=list) - - -UNIVERSAL = [ - LuaParam( - "highlight", - "string|fun(value: string): string", - "Highlight group, or function that returns a highlight group", - ), - LuaParam( - "align", - '"left"|"center"|"right"', - "Text alignment within the column", - ) -] -TIME = [ - LuaParam("format", "string", "Format string (see :help strftime)"), -] -COL_DEFS = [ - ColumnDef( - "type", - "*", - False, - True, - "The type of the entry (file, directory, link, etc)", - UNIVERSAL - + [LuaParam("icons", "table", "Mapping of entry type to icon")], - ), - ColumnDef( - "icon", - "*", - False, - False, - "An icon for the entry's type (requires nvim-web-devicons)", - UNIVERSAL - + [ - LuaParam( - "default_file", - "string", - "Fallback icon for files when nvim-web-devicons returns nil", - ), - LuaParam("directory", "string", "Icon for directories"), - LuaParam( - "add_padding", - "boolean", - "Set to false to remove the extra whitespace after the icon", - ), - ], - ), - ColumnDef("size", "files, ssh, s3", False, True, "The size of the file", UNIVERSAL + []), - ColumnDef( - "permissions", - "files, ssh", - True, - False, - "Access permissions of the file", - UNIVERSAL + [], - ), - ColumnDef( - "ctime", "files", False, True, "Change timestamp of the file", UNIVERSAL + TIME + [] - ), - ColumnDef( - "mtime", "files", False, True, "Last modified time of the file", UNIVERSAL + TIME + [] - ), - ColumnDef( - "atime", "files", False, True, "Last access time of the file", UNIVERSAL + TIME + [] - ), - ColumnDef( - "birthtime", - "files, s3", - False, - True, - "The time the file was created", - UNIVERSAL + TIME + [], - ), -] - - -def get_options_vimdoc() -> "VimdocSection": - section = VimdocSection("config", "oil-config") - config_file = os.path.join(ROOT, "lua", "oil", "config.lua") - opt_lines = read_section(config_file, r"^local default_config =", r"^}$") - lines = ["\n", ">lua\n", ' require("oil").setup({\n'] - lines.extend(indent(opt_lines, 4)) - lines.extend([" })\n", "<\n"]) - section.body = lines - return section - - -def get_options_detail_vimdoc() -> "VimdocSection": - section = VimdocSection("options", "oil-options") - section.body.append( - """ -skip_confirm_for_simple_edits *oil.skip_confirm_for_simple_edits* - type: `boolean` default: `false` - Before performing filesystem operations, Oil displays a confirmation popup to ensure - that all operations are intentional. When this option is `true`, the popup will be - skipped if the operations: - * contain no deletes - * contain no cross-adapter moves or copies (e.g. from local to ssh) - * contain at most one copy or move - * contain at most five creates - -prompt_save_on_select_new_entry *oil.prompt_save_on_select_new_entry* - type: `boolean` default: `true` - There are two cases where this option is relevant: - 1. You copy a file to a new location, then you select it and make edits before - saving. - 2. You copy a directory to a new location, then you enter the directory and make - changes before saving. - - In case 1, when you edit the file you are actually editing the original file because - oil has not yet moved/copied it to its new location. This means that the original - file will, perhaps unexpectedly, also be changed by any edits you make. - - Case 2 is similar; when you edit the directory you are again actually editing the - original location of the directory. If you add new files, those files will be - created in both the original location and the copied directory. - - When this option is `true`, Oil will prompt you to save before entering a file or - directory that is pending within oil, but does not exist on disk. -""" - ) - return section - - -def get_highlights_vimdoc() -> "VimdocSection": - section = VimdocSection("Highlights", "oil-highlights", ["\n"]) - highlights = read_nvim_json('require("oil")._get_highlights()') - for hl in highlights: - name = hl["name"] - desc = hl.get("desc") - if desc is None: - continue - section.body.append(leftright(name, f"*hl-{name}*")) - section.body.extend(wrap(desc, 4)) - section.body.append("\n") - return section - - -def load_params(params: Dict[str, Any]) -> List[LuaParam]: - ret = [] - for name, data in sorted(params.items()): - ret.append(LuaParam(name, data["type"], data["desc"])) - return ret - - -def get_actions_vimdoc() -> "VimdocSection": - section = VimdocSection("Actions", "oil-actions", ["\n"]) - section.body.append( - """The `keymaps` option in `oil.setup` allow you to create mappings using all the same parameters as |vim.keymap.set|. ->lua - keymaps = { - -- Mappings can be a string - ["~"] = "edit $HOME", - -- Mappings can be a function - ["gd"] = function() - require("oil").set_columns({ "icon", "permissions", "size", "mtime" }) - end, - -- You can pass additional opts to vim.keymap.set by using - -- a table with the mapping as the first element. - ["ff"] = { - function() - require("telescope.builtin").find_files({ - cwd = require("oil").get_current_dir() - }) - end, - mode = "n", - nowait = true, - desc = "Find files in the current directory" - }, - -- Mappings that are a string starting with "actions." will be - -- one of the built-in actions, documented below. - ["`"] = "actions.tcd", - -- Some actions have parameters. These are passed in via the `opts` key. - [":"] = { - "actions.open_cmdline", - opts = { - shorten_path = true, - modify = ":h", - }, - desc = "Open the command line with the current directory as an argument", - }, - } -""" - ) - section.body.append("\n") - section.body.extend( - wrap( - """Below are the actions that can be used in the `keymaps` section of config options. You can refer to them as strings (e.g. "actions.") or you can use the functions directly with `require("oil.actions").action_name.callback()`""" - ) - ) - section.body.append("\n") - actions = read_nvim_json('require("oil.actions")._get_actions()') - actions.sort(key=lambda a: a["name"]) - for action in actions: - if action.get("deprecated"): - continue - name = action["name"] - desc = action["desc"] - section.body.append(leftright(name, f"*actions.{name}*")) - section.body.extend(wrap(desc, 4)) - params = action.get("parameters") - if params: - section.body.append("\n") - section.body.append(" Parameters:\n") - section.body.extend( - format_vimdoc_params(load_params(params), LuaTypes(), 6) - ) - - section.body.append("\n") - return section - - -def get_columns_vimdoc() -> "VimdocSection": - section = VimdocSection("Columns", "oil-columns", ["\n"]) - section.body.extend( - wrap( - 'Columns can be specified as a string to use default arguments (e.g. `"icon"`), or as a table to pass parameters (e.g. `{"size", highlight = "Special"}`)' - ) - ) - section.body.append("\n") - for col in COL_DEFS: - section.body.append(leftright(col.name, f"*column-{col.name}*")) - section.body.extend(wrap(f"Adapters: {col.adapters}", 4)) - if col.sortable: - section.body.extend( - wrap(f"Sortable: this column can be used in view_props.sort", 4) - ) - if col.editable: - section.body.extend(wrap(f"Editable: this column is read/write", 4)) - section.body.extend(wrap(col.summary, 4)) - section.body.append("\n") - section.body.append(" Parameters:\n") - section.body.extend(format_vimdoc_params(col.params, LuaTypes(), 6)) - section.body.append("\n") - return section - - -def get_trash_vimdoc() -> "VimdocSection": - section = VimdocSection("Trash", "oil-trash", []) - section.body.append( - """ -Oil has built-in support for using the system trash. When -`delete_to_trash = true`, any deleted files will be sent to the trash instead -of being permanently deleted. You can browse the trash for a directory using -the `toggle_trash` action (bound to `g\\` by default). You can view all files -in the trash with `:Oil --trash /`. - -To restore files, simply move them from the trash to the desired destination, -the same as any other file operation. If you delete files from the trash they -will be permanently deleted (purged). - -Linux: - Oil supports the FreeDesktop trash specification. - https://specifications.freedesktop.org/trash-spec/1.0/ - All features should work. - -Mac: - Oil has limited support for MacOS due to the proprietary nature of the - implementation. The trash bin can only be viewed as a single dir - (instead of being able to see files that were trashed from a directory). - -Windows: - Oil supports the Windows Recycle Bin. All features should work. -""" - ) - return section - - -def generate_vimdoc(): - doc = Vimdoc("oil.txt", "oil") - types = parse_directory(os.path.join(ROOT, "lua")) - funcs = types.files["oil/init.lua"].functions - doc.sections.extend( - [ - get_options_vimdoc(), - get_options_detail_vimdoc(), - VimdocSection("API", "oil-api", render_vimdoc_api2("oil", funcs, types)), - get_columns_vimdoc(), - get_actions_vimdoc(), - get_highlights_vimdoc(), - get_trash_vimdoc(), - ] - ) - - with open(VIMDOC, "w", encoding="utf-8") as ofile: - ofile.writelines(doc.render()) - - -def main() -> None: - """Update the README""" - update_config_options() - update_md_api() - update_md_toc(README, max_level=1) - update_md_toc(os.path.join(DOC, "recipes.md")) - update_readme() - generate_vimdoc() diff --git a/scripts/requirements.txt b/scripts/requirements.txt deleted file mode 100644 index 2c6271f..0000000 --- a/scripts/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -pyparsing==3.0.9 -black -isort -mypy diff --git a/syntax/oil_preview.vim b/syntax/oil_preview.vim index 2f14df9..41656ee 100644 --- a/syntax/oil_preview.vim +++ b/syntax/oil_preview.vim @@ -2,14 +2,10 @@ if exists("b:current_syntax") finish endif -syn match oilCreate /^CREATE\( BUCKET\)\? / -syn match oilMove /^ MOVE / -syn match oilDelete /^DELETE\( BUCKET\)\? / -syn match oilCopy /^ COPY / +syn match oilCreate /^CREATE / +syn match oilMove /^ MOVE / +syn match oilDelete /^DELETE / +syn match oilCopy /^ COPY / syn match oilChange /^CHANGE / -" Trash operations -syn match oilRestore /^RESTORE / -syn match oilPurge /^ PURGE / -syn match oilTrash /^ TRASH / let b:current_syntax = "oil_preview" diff --git a/tests/altbuf_spec.lua b/tests/altbuf_spec.lua index beece15..df481f1 100644 --- a/tests/altbuf_spec.lua +++ b/tests/altbuf_spec.lua @@ -1,7 +1,7 @@ require("plenary.async").tests.add_to_env() -local fs = require("oil.fs") local oil = require("oil") local test_util = require("tests.test_util") +local fs = require("oil.fs") a.describe("Alternate buffer", function() after_each(function() @@ -11,7 +11,7 @@ a.describe("Alternate buffer", function() a.it("sets previous buffer as alternate", function() vim.cmd.edit({ args = { "foo" } }) oil.open() - test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) + test_util.wait_for_autocmd("BufReadPost") vim.cmd.edit({ args = { "bar" } }) assert.equals("foo", vim.fn.expand("#")) end) @@ -19,7 +19,7 @@ a.describe("Alternate buffer", function() 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" }) + test_util.wait_for_autocmd("BufReadPost") 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 @@ -32,7 +32,7 @@ a.describe("Alternate buffer", function() 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" }) + test_util.wait_for_autocmd("BufReadPost") vim.cmd.edit({ args = { "bar" } }) assert.equals("foo", vim.fn.expand("#")) end) @@ -41,7 +41,7 @@ a.describe("Alternate buffer", function() vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "bar" } }) oil.open() - test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) + test_util.wait_for_autocmd("BufReadPost") vim.cmd.edit({ args = { "bar" } }) assert.equals("foo", vim.fn.expand("#")) end) @@ -50,7 +50,7 @@ a.describe("Alternate buffer", function() vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "bar" } }) oil.open() - test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) + test_util.wait_for_autocmd("BufReadPost") oil.close() assert.equals("bar", vim.fn.expand("%")) assert.equals("foo", vim.fn.expand("#")) @@ -59,13 +59,13 @@ a.describe("Alternate buffer", function() 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" }) + test_util.wait_for_autocmd("BufReadPost") oil.open() - test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) + test_util.wait_for_autocmd("BufReadPost") oil.open() - test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) + test_util.wait_for_autocmd("BufReadPost") oil.open() - test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) + test_util.wait_for_autocmd("BufReadPost") vim.cmd.edit({ args = { "bar" } }) assert.equals("foo", vim.fn.expand("#")) end) @@ -73,7 +73,7 @@ a.describe("Alternate buffer", function() 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" }) + test_util.wait_for_autocmd("BufReadPost") assert.equals("foo", vim.fn.expand("#")) vim.cmd.edit({ args = { "bar" } }) assert.equals("foo", vim.fn.expand("#")) @@ -84,28 +84,28 @@ a.describe("Alternate buffer", function() a.it("preserves alternate when traversing oil dirs", function() vim.cmd.edit({ args = { "foo" } }) oil.open() - test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) + test_util.wait_for_autocmd("BufReadPost") 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" }) + test_util.wait_for_autocmd("BufReadPost") 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" }) + test_util.wait_for_autocmd("BufReadPost") 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" }) + oil.select({ preview = true }) + test_util.wait_for_autocmd("BufReadPost") assert.equals("foo", vim.fn.expand("#")) end) @@ -113,7 +113,7 @@ a.describe("Alternate buffer", 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" }) + test_util.wait_for_autocmd("BufReadPost") -- 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) @@ -125,7 +125,7 @@ a.describe("Alternate buffer", function() vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "bar" } }) oil.open_float() - test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) + test_util.wait_for_autocmd("BufReadPost") -- 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) @@ -137,21 +137,9 @@ a.describe("Alternate buffer", function() vim.cmd.edit({ args = { "foo" } }) vim.cmd.edit({ args = { "bar" } }) oil.open_float() - test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) + test_util.wait_for_autocmd("BufReadPost") oil.close() assert.equals("foo", vim.fn.expand("#")) end) - - a.it("preserves alternate when traversing to a new file", function() - vim.cmd.edit({ args = { "foo" } }) - oil.open_float() - test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) - assert.equals("foo", vim.fn.expand("#")) - test_util.feedkeys({ "/LICENSE" }, 10) - oil.select() - test_util.wait_for_autocmd("BufEnter") - assert.equals("LICENSE", vim.fn.expand("%:.")) - assert.equals("foo", vim.fn.expand("#")) - end) end) end) diff --git a/tests/files_spec.lua b/tests/files_spec.lua index 4333d80..72533e4 100644 --- a/tests/files_spec.lua +++ b/tests/files_spec.lua @@ -1,7 +1,7 @@ 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") +local TmpDir = require("tests.tmpdir") a.describe("files adapter", function() local tmpdir @@ -11,6 +11,8 @@ a.describe("files adapter", function() a.after_each(function() if tmpdir then tmpdir:dispose() + a.util.scheduler() + tmpdir = nil end test_util.reset_editor() end) @@ -150,10 +152,10 @@ a.describe("files adapter", function() a.it("Editing a new oil://path/ creates an oil buffer", function() local tmpdir_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "/" vim.cmd.edit({ args = { tmpdir_url } }) - test_util.wait_oil_ready() + test_util.wait_for_autocmd("BufReadPost") local new_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "newdir" vim.cmd.edit({ args = { new_url } }) - test_util.wait_oil_ready() + test_util.wait_for_autocmd("BufReadPost") assert.equals("oil", vim.bo.filetype) -- The normalization will add a '/' assert.equals(new_url .. "/", vim.api.nvim_buf_get_name(0)) @@ -168,6 +170,5 @@ a.describe("files adapter", function() test_util.wait_for_autocmd("BufReadPost") assert.equals("ruby", vim.bo.filetype) assert.equals(vim.fn.fnamemodify(tmpdir.path, ":p") .. "file.rb", vim.api.nvim_buf_get_name(0)) - assert.equals(tmpdir.path .. "/file.rb", vim.fn.bufname()) end) end) diff --git a/tests/manual_progress.lua b/tests/manual_progress.lua deleted file mode 100644 index bb838e2..0000000 --- a/tests/manual_progress.lua +++ /dev/null @@ -1,28 +0,0 @@ --- Manual test for minimizing/restoring progress window -local Progress = require("oil.mutator.progress") - -local progress = Progress.new() - -progress:show({ - cancel = function() - progress:close() - end, -}) - -for i = 1, 10, 1 do - vim.defer_fn(function() - progress:set_action({ - type = "create", - url = string.format("oil:///tmp/test_%d.txt", i), - entry_type = "file", - }, i, 10) - end, (i - 1) * 1000) -end - -vim.defer_fn(function() - progress:close() -end, 10000) - -vim.keymap.set("n", "R", function() - progress:restore() -end, {}) diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index 70a66b1..262d9ec 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -1,4 +1,4 @@ -vim.opt.runtimepath:append(".") +vim.cmd([[set runtimepath+=.]]) vim.o.swapfile = false vim.bo.swapfile = false diff --git a/tests/mutator_spec.lua b/tests/mutator_spec.lua index 17548d3..00e8932 100644 --- a/tests/mutator_spec.lua +++ b/tests/mutator_spec.lua @@ -2,16 +2,240 @@ require("plenary.async").tests.add_to_env() local cache = require("oil.cache") local constants = require("oil.constants") local mutator = require("oil.mutator") +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_NAME = constants.FIELD_NAME local FIELD_TYPE = constants.FIELD_TYPE +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 a.describe("mutator", function() after_each(function() - test_util.reset_editor() + for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do + vim.api.nvim_buf_delete(bufnr, { force = true }) + end + cache.clear_everything() + end) + + describe("parser", function() + 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 = cache.create_and_store_entry("oil-test:///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 = cache.create_and_store_entry("oil-test:///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 = cache.create_and_store_entry("oil-test:///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 = cache.create_and_store_entry("oil-test:///foo/", "a.txt", "file") + vim.cmd.edit({ args = { "oil-test:///foo/" } }) + local bufnr = vim.api.nvim_get_current_buf() + local cols = view.format_entry_cols(file, {}, {}, test_adapter) + local 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, + 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, + 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, + col = 0, + }, + }, errors) + end) + + it("errors on duplicate names for existing files", function() + local file = cache.create_and_store_entry("oil-test:///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, + 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 = cache.create_and_store_entry("oil-test:///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 renamed files that conflict", function() + local afile = cache.create_and_store_entry("oil-test:///foo/", "a.txt", "file") + local bfile = cache.create_and_store_entry("oil-test:///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 = cache.create_and_store_entry("oil-test:///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) describe("build actions", function() @@ -43,7 +267,7 @@ a.describe("mutator", function() end) it("constructs DELETE actions", function() - local file = test_adapter.test_set("/foo/a.txt", "file") + local file = cache.create_and_store_entry("oil-test:///foo/", "a.txt", "file") vim.cmd.edit({ args = { "oil-test:///foo/" } }) local bufnr = vim.api.nvim_get_current_buf() local diffs = { @@ -62,7 +286,7 @@ a.describe("mutator", function() end) it("constructs COPY actions", function() - local file = test_adapter.test_set("/foo/a.txt", "file") + local file = cache.create_and_store_entry("oil-test:///foo/", "a.txt", "file") vim.cmd.edit({ args = { "oil-test:///foo/" } }) local bufnr = vim.api.nvim_get_current_buf() local diffs = { @@ -82,7 +306,7 @@ a.describe("mutator", function() end) it("constructs MOVE actions", function() - local file = test_adapter.test_set("/foo/a.txt", "file") + local file = cache.create_and_store_entry("oil-test:///foo/", "a.txt", "file") vim.cmd.edit({ args = { "oil-test:///foo/" } }) local bufnr = vim.api.nvim_get_current_buf() local diffs = { @@ -103,7 +327,7 @@ a.describe("mutator", function() end) it("correctly orders MOVE + CREATE", function() - local file = test_adapter.test_set("/a.txt", "file") + local file = cache.create_and_store_entry("oil-test:///", "a.txt", "file") vim.cmd.edit({ args = { "oil-test:///" } }) local bufnr = vim.api.nvim_get_current_buf() local diffs = { @@ -130,8 +354,8 @@ a.describe("mutator", function() end) it("resolves MOVE loops", function() - local afile = test_adapter.test_set("/a.txt", "file") - local bfile = test_adapter.test_set("/b.txt", "file") + local afile = cache.create_and_store_entry("oil-test:///", "a.txt", "file") + local bfile = cache.create_and_store_entry("oil-test:///", "b.txt", "file") vim.cmd.edit({ args = { "oil-test:///" } }) local bufnr = vim.api.nvim_get_current_buf() local diffs = { @@ -182,19 +406,6 @@ a.describe("mutator", function() 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 @@ -216,26 +427,6 @@ a.describe("mutator", function() assert.are.same({ move1, move2 }, ordered_actions) end) - it("Handles a delete inside a moved folder", function() - -- delete in directory and move directory - -- DELETE /a/b.txt - -- MOVE /a/ -> /b/ - local del = { - type = "delete", - url = "oil-test:///a/b.txt", - entry_type = "file", - } - local move = { - type = "move", - src_url = "oil-test:///a", - dest_url = "oil-test:///b", - entry_type = "directory", - } - local actions = { move, del } - local ordered_actions = mutator.enforce_action_order(actions) - assert.are.same({ del, move }, ordered_actions) - end) - it("Detects move directory loops", function() local move = { type = "move", @@ -363,7 +554,7 @@ a.describe("mutator", function() end) a.it("deletes entries", function() - local file = test_adapter.test_set("/a.txt", "file") + local file = cache.create_and_store_entry("oil-test:///", "a.txt", "file") local actions = { { type = "delete", url = "oil-test:///a.txt", entry_type = "file" }, } @@ -377,7 +568,7 @@ a.describe("mutator", function() end) a.it("moves entries", function() - local file = test_adapter.test_set("/a.txt", "file") + local file = cache.create_and_store_entry("oil-test:///", "a.txt", "file") local actions = { { type = "move", @@ -401,7 +592,7 @@ a.describe("mutator", function() end) a.it("copies entries", function() - local file = test_adapter.test_set("/a.txt", "file") + local file = cache.create_and_store_entry("oil-test:///", "a.txt", "file") local actions = { { type = "copy", diff --git a/tests/parser_spec.lua b/tests/parser_spec.lua deleted file mode 100644 index 9884ca1..0000000 --- a/tests/parser_spec.lua +++ /dev/null @@ -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) diff --git a/tests/preview_spec.lua b/tests/preview_spec.lua deleted file mode 100644 index 08aba78..0000000 --- a/tests/preview_spec.lua +++ /dev/null @@ -1,41 +0,0 @@ -require("plenary.async").tests.add_to_env() -local TmpDir = require("tests.tmpdir") -local oil = require("oil") -local test_util = require("tests.test_util") -local util = require("oil.util") - -a.describe("oil preview", function() - local tmpdir - a.before_each(function() - tmpdir = TmpDir.new() - end) - a.after_each(function() - if tmpdir then - tmpdir:dispose() - end - test_util.reset_editor() - end) - - a.it("opens preview window", function() - tmpdir:create({ "a.txt" }) - test_util.oil_open(tmpdir.path) - a.wrap(oil.open_preview, 2)() - local preview_win = util.get_preview_win() - assert.not_nil(preview_win) - assert(preview_win) - local bufnr = vim.api.nvim_win_get_buf(preview_win) - local preview_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.same({ "a.txt" }, preview_lines) - end) - - a.it("opens preview window when open(preview={})", function() - tmpdir:create({ "a.txt" }) - test_util.oil_open(tmpdir.path, { preview = {} }) - local preview_win = util.get_preview_win() - assert.not_nil(preview_win) - assert(preview_win) - local bufnr = vim.api.nvim_win_get_buf(preview_win) - local preview_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.same({ "a.txt" }, preview_lines) - end) -end) diff --git a/tests/regression_spec.lua b/tests/regression_spec.lua index 9d8f944..380a3ee 100644 --- a/tests/regression_spec.lua +++ b/tests/regression_spec.lua @@ -1,9 +1,7 @@ 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") +local TmpDir = require("tests.tmpdir") a.describe("regression tests", function() local tmpdir @@ -13,6 +11,7 @@ a.describe("regression tests", function() a.after_each(function() if tmpdir then tmpdir:dispose() + a.util.scheduler() tmpdir = nil end test_util.reset_editor() @@ -27,21 +26,19 @@ a.describe("regression tests", function() 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" }) + test_util.wait_for_autocmd("BufReadPost") 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" }) + test_util.wait_for_autocmd("BufReadPost") 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" }) + a.util.sleep(10) entry = oil.get_cursor_entry() assert.equals("README.md", entry and entry.name) end) @@ -80,7 +77,7 @@ a.describe("regression tests", function() -- 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" }) + test_util.wait_for_autocmd("BufReadPost") oil.close() assert.not_equals("oil", vim.bo.filetype) assert.equals("", vim.api.nvim_buf_get_name(0)) @@ -91,7 +88,7 @@ a.describe("regression tests", function() 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.wait_for_autocmd("BufReadPost") test_util.feedkeys({ "dd", "itest/", "" }, 10) vim.wait(1000, function() return vim.bo.modifiable @@ -111,38 +108,10 @@ a.describe("regression tests", function() a.it("refreshing buffer doesn't lose track of it", function() vim.cmd.edit({ args = { "." } }) - test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) + test_util.wait_for_autocmd("BufReadPost") local bufnr = vim.api.nvim_get_current_buf() vim.cmd.edit({ bang = true }) - test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) + test_util.wait_for_autocmd("BufReadPost") 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) diff --git a/tests/select_spec.lua b/tests/select_spec.lua index 9de2eb4..d4d3700 100644 --- a/tests/select_spec.lua +++ b/tests/select_spec.lua @@ -8,7 +8,8 @@ a.describe("oil select", function() end) a.it("opens file under cursor", function() - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) -- Go to the bottom, so the cursor is not on a directory vim.cmd.normal({ args = { "G" } }) a.wrap(oil.select, 2)() @@ -17,7 +18,8 @@ a.describe("oil select", function() end) a.it("opens file in new tab", function() - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) local tabpage = vim.api.nvim_get_current_tabpage() a.wrap(oil.select, 2)({ tab = true }) assert.equals(2, #vim.api.nvim_list_tabpages()) @@ -26,7 +28,8 @@ a.describe("oil select", function() end) a.it("opens file in new split", function() - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) local winid = vim.api.nvim_get_current_win() a.wrap(oil.select, 2)({ vertical = true }) assert.equals(1, #vim.api.nvim_list_tabpages()) @@ -35,7 +38,8 @@ a.describe("oil select", function() end) a.it("opens multiple files in new tabs", function() - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) vim.api.nvim_feedkeys("Vj", "x", true) local tabpage = vim.api.nvim_get_current_tabpage() a.wrap(oil.select, 2)({ tab = true }) @@ -45,7 +49,8 @@ a.describe("oil select", function() end) a.it("opens multiple files in new splits", function() - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) vim.api.nvim_feedkeys("Vj", "x", true) local winid = vim.api.nvim_get_current_win() a.wrap(oil.select, 2)({ vertical = true }) @@ -58,7 +63,8 @@ a.describe("oil select", function() a.it("same window", function() vim.cmd.edit({ args = { "foo" } }) local bufnr = vim.api.nvim_get_current_buf() - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) -- Go to the bottom, so the cursor is not on a directory vim.cmd.normal({ args = { "G" } }) a.wrap(oil.select, 2)({ close = true }) @@ -73,7 +79,8 @@ a.describe("oil select", function() vim.cmd.edit({ args = { "foo" } }) local bufnr = vim.api.nvim_get_current_buf() local winid = vim.api.nvim_get_current_win() - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) a.wrap(oil.select, 2)({ vertical = true, close = true }) assert.equals(2, #vim.api.nvim_tabpage_list_wins(0)) assert.equals(bufnr, vim.api.nvim_win_get_buf(winid)) @@ -83,7 +90,8 @@ a.describe("oil select", function() vim.cmd.edit({ args = { "foo" } }) local bufnr = vim.api.nvim_get_current_buf() local tabpage = vim.api.nvim_get_current_tabpage() - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) a.wrap(oil.select, 2)({ tab = true, close = true }) assert.equals(1, #vim.api.nvim_tabpage_list_wins(0)) assert.equals(2, #vim.api.nvim_list_tabpages()) diff --git a/tests/test_util.lua b/tests/test_util.lua index 46f63df..63c55ca 100644 --- a/tests/test_util.lua +++ b/tests/test_util.lua @@ -1,7 +1,5 @@ require("plenary.async").tests.add_to_env() local cache = require("oil.cache") -local test_adapter = require("oil.adapters.test") -local util = require("oil.util") local M = {} M.reset_editor = function() @@ -23,23 +21,6 @@ M.reset_editor = function() vim.api.nvim_buf_delete(bufnr, { force = true }) end cache.clear_everything() - test_adapter.test_clear() -end - -local function throwiferr(err, ...) - if err then - error(err) - else - return ... - end -end - -M.oil_open = function(...) - a.wrap(require("oil").open, 3)(...) -end - -M.await = function(fn, nargs, ...) - return throwiferr(a.wrap(fn, nargs)(...)) end M.wait_for_autocmd = a.wrap(function(autocmd, cb) @@ -58,10 +39,6 @@ M.wait_for_autocmd = a.wrap(function(autocmd, cb) vim.api.nvim_create_autocmd(autocmd, opts) end, 2) -M.wait_oil_ready = a.wrap(function(cb) - util.run_after_load(0, vim.schedule_wrap(cb)) -end, 1) - ---@param actions string[] ---@param timestep integer M.feedkeys = function(actions, timestep) @@ -79,62 +56,4 @@ M.feedkeys = function(actions, timestep) a.util.sleep(timestep) end -M.actions = { - ---Open oil and wait for it to finish rendering - ---@param args string[] - open = function(args) - vim.schedule(function() - vim.cmd.Oil({ args = args }) - -- If this buffer was already open, manually dispatch the autocmd to finish the wait - if vim.b.oil_ready then - vim.api.nvim_exec_autocmds("User", { - pattern = "OilEnter", - modeline = false, - data = { buf = vim.api.nvim_get_current_buf() }, - }) - end - end) - M.wait_for_autocmd({ "User", pattern = "OilEnter" }) - end, - - ---Save all changes and wait for operation to complete - save = function() - vim.schedule_wrap(require("oil").save)({ confirm = false }) - M.wait_for_autocmd({ "User", pattern = "OilMutationComplete" }) - end, - - ---@param bufnr? integer - reload = function(bufnr) - M.await(require("oil.view").render_buffer_async, 3, bufnr or 0) - end, - - ---Move cursor to a file or directory in an oil buffer - ---@param filename string - focus = function(filename) - local lines = vim.api.nvim_buf_get_lines(0, 0, -1, true) - local search = " " .. filename .. "$" - for i, line in ipairs(lines) do - if line:match(search) then - vim.api.nvim_win_set_cursor(0, { i, 0 }) - return - end - end - error("Could not find file " .. filename) - end, -} - ----Get the raw list of filenames from an unmodified oil buffer ----@param bufnr? integer ----@return string[] -M.parse_entries = function(bufnr) - bufnr = bufnr or 0 - if vim.bo[bufnr].modified then - error("parse_entries doesn't work on a modified oil buffer") - end - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) - return vim.tbl_map(function(line) - return line:match("^/%d+ +(.+)$") - end, lines) -end - return M diff --git a/tests/tmpdir.lua b/tests/tmpdir.lua index bb8a9c0..62018f4 100644 --- a/tests/tmpdir.lua +++ b/tests/tmpdir.lua @@ -1,7 +1,16 @@ local fs = require("oil.fs") -local test_util = require("tests.test_util") -local await = test_util.await +local function throwiferr(err, ...) + if err then + error(err) + else + return ... + end +end + +local function await(fn, nargs, ...) + return throwiferr(a.wrap(fn, nargs)(...)) +end ---@param path string ---@param cb fun(err: nil|string) @@ -32,7 +41,6 @@ local TmpDir = {} TmpDir.new = function() local path = await(vim.loop.fs_mkdtemp, 2, "oil_test_XXXXXXXXX") - a.util.scheduler() return setmetatable({ path = path }, { __index = TmpDir, }) @@ -52,7 +60,6 @@ function TmpDir:create(paths) end end end - a.util.scheduler() end ---@param filepath string @@ -65,7 +72,6 @@ local read_file = function(filepath) local stat = vim.loop.fs_fstat(fd) local content = vim.loop.fs_read(fd, stat.size) vim.loop.fs_close(fd) - a.util.scheduler() return content end @@ -93,9 +99,9 @@ local assert_fs = function(root, paths) local pieces = vim.split(k, "/") local partial_path = "" for i, piece in ipairs(pieces) do - partial_path = partial_path .. piece .. "/" + partial_path = fs.join(partial_path, piece) .. "/" if i ~= #pieces then - unlisted_dirs[partial_path] = true + unlisted_dirs[partial_path:sub(2)] = true end end end @@ -146,23 +152,8 @@ function TmpDir:assert_fs(paths) assert_fs(self.path, paths) end -function TmpDir:assert_exists(path) - a.util.scheduler() - path = fs.join(self.path, path) - local stat = vim.loop.fs_stat(path) - assert.truthy(stat, string.format("Expected path '%s' to exist", path)) -end - -function TmpDir:assert_not_exists(path) - a.util.scheduler() - path = fs.join(self.path, path) - local stat = vim.loop.fs_stat(path) - assert.falsy(stat, string.format("Expected path '%s' to not exist", path)) -end - function TmpDir:dispose() await(fs.recursive_delete, 3, "directory", self.path) - a.util.scheduler() end return TmpDir diff --git a/tests/trash_spec.lua b/tests/trash_spec.lua deleted file mode 100644 index d09a57f..0000000 --- a/tests/trash_spec.lua +++ /dev/null @@ -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) diff --git a/tests/url_spec.lua b/tests/url_spec.lua index 32d801e..fbdde90 100644 --- a/tests/url_spec.lua +++ b/tests/url_spec.lua @@ -4,7 +4,7 @@ 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/" }, + { "term://~/oil.nvim//52953:/bin/bash", "oil://" .. vim.loop.os_homedir() .. "/oil.nvim/" }, { "/foo/bar.txt", "oil:///foo/", "bar.txt" }, { "oil:///foo/bar.txt", "oil:///foo/", "bar.txt" }, { "oil:///", "oil:///" }, @@ -13,7 +13,7 @@ describe("url", function() } for _, case in ipairs(cases) do local input, expected, expected_basename = unpack(case) - local output, basename = oil.get_buffer_parent_url(input, true) + local output, basename = oil.get_buffer_parent_url(input) assert.equals(expected, output, string.format('Parent url for path "%s" failed', input)) assert.equals( expected_basename, diff --git a/tests/util_spec.lua b/tests/util_spec.lua deleted file mode 100644 index 3193842..0000000 --- a/tests/util_spec.lua +++ /dev/null @@ -1,29 +0,0 @@ -local util = require("oil.util") -describe("util", function() - it("url_escape", function() - local cases = { - { "foobar", "foobar" }, - { "foo bar", "foo%20bar" }, - { "/foo/bar", "%2Ffoo%2Fbar" }, - } - for _, case in ipairs(cases) do - local input, expected = unpack(case) - local output = util.url_escape(input) - assert.equals(expected, output) - end - end) - - it("url_unescape", function() - local cases = { - { "foobar", "foobar" }, - { "foo%20bar", "foo bar" }, - { "%2Ffoo%2Fbar", "/foo/bar" }, - { "foo%%bar", "foo%%bar" }, - } - for _, case in ipairs(cases) do - local input, expected = unpack(case) - local output = util.url_unescape(input) - assert.equals(expected, output) - end - end) -end) diff --git a/tests/win_options_spec.lua b/tests/win_options_spec.lua index cb638a7..49ab811 100644 --- a/tests/win_options_spec.lua +++ b/tests/win_options_spec.lua @@ -9,28 +9,32 @@ a.describe("window options", function() a.it("Restores window options on close", function() vim.cmd.edit({ args = { "README.md" } }) - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd("BufReadPost") assert.equals("no", vim.o.signcolumn) oil.close() assert.equals("auto", vim.o.signcolumn) end) a.it("Restores window options on edit", function() - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd("BufReadPost") assert.equals("no", vim.o.signcolumn) vim.cmd.edit({ args = { "README.md" } }) assert.equals("auto", vim.o.signcolumn) end) a.it("Restores window options on split ", function() - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd("BufReadPost") assert.equals("no", vim.o.signcolumn) vim.cmd.split({ args = { "README.md" } }) assert.equals("auto", vim.o.signcolumn) end) a.it("Restores window options on split", function() - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd("BufReadPost") assert.equals("no", vim.o.signcolumn) vim.cmd.split() vim.cmd.edit({ args = { "README.md" } }) @@ -38,14 +42,16 @@ a.describe("window options", function() end) a.it("Restores window options on tabnew ", function() - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd("BufReadPost") assert.equals("no", vim.o.signcolumn) vim.cmd.tabnew({ args = { "README.md" } }) assert.equals("auto", vim.o.signcolumn) end) a.it("Restores window options on tabnew", function() - test_util.oil_open() + oil.open() + test_util.wait_for_autocmd("BufReadPost") assert.equals("no", vim.o.signcolumn) vim.cmd.tabnew() vim.cmd.edit({ args = { "README.md" } }) @@ -54,7 +60,7 @@ a.describe("window options", function() a.it("Sets the window options when re-entering oil buffer", function() oil.open() - test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) + test_util.wait_for_autocmd("BufReadPost") assert.truthy(vim.w.oil_did_enter) vim.cmd.edit({ args = { "README.md" } }) assert.falsy(vim.w.oil_did_enter)