Compare commits

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

88 commits

Author SHA1 Message Date
8e84d94975
docs(upstream): mark #637 not actionable
Problem: upstream issue #637 (inconsistent symlink resolution) is
unresolved in our tracker.

Solution: mark as not actionable. Reporter confirmed the issue does
not reproduce on stable neovim (v0.11.2), only on nightly.
2026-03-09 18:44:39 -04:00
Barrett Ruth
68c4a1b0c7
docs(upstream): mark #636 not actionable (#110)
Problem: upstream issue #636 (Telescope picker opens file in wrong
split) is unresolved in our tracker.

Solution: mark as not actionable. Cannot reproduce with default
Telescope configuration — file correctly opens in the active canola
split. Likely user-specific Telescope config or window picker plugin.
2026-03-09 18:12:30 -04:00
Barrett Ruth
c7b905137c
fix: make parent action a no-op at filesystem root (#109)
Problem: at the filesystem root (`/`), `actions.parent` triggers a
full `vim.cmd.edit()` and async re-render cycle even though the parent
of `/` is `/`.

Solution: in `canola.open()`, return early when `parent_url` equals
the current buffer name.

Closes #108.
2026-03-09 18:05:04 -04:00
Barrett Ruth
39374ee99b
fix(select): redraw screen after buffer switch (#106)
* fix(select): redraw screen after buffer switch

Problem: `select` opens files inside a `vim.schedule_wrap` callback
from `normalize_url`. Scheduled `FileType` autocmds (e.g. treesitter
parsing) queue onto the same batch, blocking the screen update. The
oil buffer stays visible until the heavy work finishes.

Solution: call `vim.cmd.redraw()` after the buffer switch to flush
the screen before any queued scheduled callbacks run. Matches the
behavior of plain `:e`.

* docs(upstream): mark #699 fixed (#106)
2026-03-09 17:40:19 -04:00
Barrett Ruth
03c2ac4bd5
docs(upstream): mark #609 not actionable (#105)
docs(upstream): mark #609 not actionable (#105)

Problem: upstream issue #609 (cursor not placed on file when jumped
via Snacks.nvim picker) is confirmed Windows-only by multiple
reporters. stevearc cannot reproduce on Linux.

Solution: mark as not actionable in the upstream tracker.
2026-03-09 17:17:40 -04:00
Barrett Ruth
c42c087ffb
docs(upstream): revert #736 to open (#104) 2026-03-08 19:12:32 -04:00
Barrett Ruth
7b140a7f40
docs(upstream): mark #736 not actionable (#103) 2026-03-08 19:08:31 -04:00
Barrett Ruth
21d9ef0c1d
docs(upstream): mark #685 not actionable (#102) 2026-03-08 19:08:17 -04:00
Barrett Ruth
f7970e1c1c
docs(upstream): mark #671 not actionable (#101) 2026-03-08 19:08:03 -04:00
Barrett Ruth
514d12a437
docs(upstream): mark #668 not actionable (#100) 2026-03-08 19:07:51 -04:00
Barrett Ruth
379c72a2bc
docs(upstream): mark #665 not actionable (#99) 2026-03-08 19:06:14 -04:00
Barrett Ruth
725dec0185
docs(upstream): mark #294 not actionable (#98) 2026-03-08 19:05:58 -04:00
Barrett Ruth
70364d1667
docs(upstream): mark #276 not actionable (#97) 2026-03-08 19:05:37 -04:00
Barrett Ruth
01a7b891ad
docs(upstream): mark #226 not actionable (#96) 2026-03-08 19:05:21 -04:00
Barrett Ruth
3249bbfaa7
docs(upstream): mark #207 fixed (#94) 2026-03-08 16:03:05 -04:00
Barrett Ruth
425a53d2fa
fix(view): constrain cursor in insert mode (#93)
Problem: `constrain_cursor` only fired on `CursorMoved` and
`ModeChanged`, so arrow key navigation in insert mode could move
the cursor into the concealed ID prefix area.

Solution: add `CursorMovedI` to the autocmd event list. The
`constrain_cursor()` function is already mode-agnostic.
2026-03-08 16:02:09 -04:00
Barrett Ruth
c972bbe9c4
docs(upstream): mark #359 not actionable (#92) 2026-03-08 16:00:31 -04:00
Barrett Ruth
ac48ce20f5
docs(upstream): collapse issue sections into single table (#91)
Problem: issues were split across four separate sections by status,
requiring moves between sections on every status change.

Solution: merge all issues into one `## Issues` table sorted by number
with an inline status column. Update digest script heading and row
format to match.
2026-03-08 15:52:25 -04:00
Barrett Ruth
0424ab3e65
fix(ci): update digest script for reorganized upstream tracker (#90)
Problem: the upstream digest script referenced old section headings
(`## Open upstream PRs`, `## Upstream issues`) and a 3-column issue
row format that no longer exist after the tracker reorganization.

Solution: update headings to `## Upstream PRs` and `## Issues — open`,
and change issue row format to the new 2-column layout.
2026-03-08 15:44:32 -04:00
Barrett Ruth
76e1aacde0
docs(upstream): reorganize tracker into grouped tables (#89)
Problem: the upstream tracker had duplicate entries across tables,
fragile commit hash references, a 108-row flat table mixing all
statuses, and inconsistent formatting.

Solution: merge PR tables into one, split issues by status (fixed,
resolved, open, not actionable), drop all commit hashes in favor of
stable PR numbers, and eliminate duplication so each entry appears
exactly once.
2026-03-08 15:41:37 -04:00
Barrett Ruth
94db584f81
feat: add toggle() API for regular windows (#88)
* feat: add `toggle()` API for regular windows

Problem: `toggle_float()` and `toggle_split()` exist but there is no
`toggle()` for regular windows, forcing users to write their own
filetype-checking wrapper.

Solution: add `M.toggle()` that delegates to `close()` or `open()`
based on whether the current buffer is a canola buffer. Includes
vimdoc entry.

* docs(upstream): mark #621 fixed
2026-03-08 15:33:45 -04:00
Barrett Ruth
fc43684bbd
fix(columns): hide misleading directory sizes (#87)
* fix(columns): hide misleading directory sizes in size column

Problem: the size column shows the filesystem inode size (typically
4096 = 4.1k) for directories, which is misleading — users expect no
size for directories.

Solution: add an early return for directory entries in the size render
function of the files, SSH, and S3 adapters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(upstream): mark #486 fixed

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:31:43 -04:00
Barrett Ruth
abc4879688
Fix require statement for oil.nvim setup 2026-03-07 21:12:45 -05:00
Barrett Ruth
91562016c8
docs(upstream): mark #380 not actionable (#86) 2026-03-07 17:05:46 -05:00
Barrett Ruth
4a8d57a269
feat: add max_file_size preview limit and show_hidden_when_empty (#85)
* feat(preview): add `max_file_size` config to skip large file previews

Problem: previewing large files (e.g. 500 MB logs, binaries) loads them
into a buffer and can freeze or OOM Neovim. `disable_preview` only
receives the filename, so users cannot gate on file size.

Solution: add `preview_win.max_file_size` (number, MB, default 10). In
`open_preview`, check `entry.meta.stat.size` and fall back to
`vim.uv.fs_stat` when the cached stat is absent. If the file exceeds
the limit and a preview window is already open, render "File too large
to preview" in it; if not, emit a WARN notify and return early. The
cursor-moved auto-update path only fires when a window already exists,
so no flag threading is needed to distinguish explicit from implicit.

Based on: stevearc/oil.nvim#213

* feat(view): add `show_hidden_when_empty` for hidden-only directories

Problem: with `show_hidden = false`, a directory containing only
dotfiles renders as just `..`, giving no indication that entries exist.

Solution: add `view_options.show_hidden_when_empty` (boolean, default
false). After the main filter loop in `render_buffer`, if the option is
set and `#line_table <= 1`, iterate `entry_list` again and render any
entry not matched by `is_always_hidden`, using `is_hidden = true` so
they render with the dimmed hidden style.

Based on: stevearc/oil.nvim#473

* docs(upstream): fix formatting

* docs(upstream): update #213 and #473 with PR and commit links
2026-03-07 16:52:57 -05:00
Barrett Ruth
a9a06b8f3b
feat: add auto_save_on_select_new_entry config option (#84)
Problem: users who want hands-off behaviour had no way to skip the
`prompt_save_on_select_new_entry` confirmation dialog — enabling the
prompt meant always being asked, with no silent auto-save path.

Solution: add `auto_save_on_select_new_entry` (default `false`) which,
when true, calls `M.save()` and proceeds immediately instead of showing
the confirm dialog. Includes type annotations, vimdoc, and upstream
tracker update for stevearc/oil.nvim#393.
2026-03-07 16:08:34 -05:00
Barrett Ruth
082573d779
feat: add open_split/toggle_split API and upstream triage batch (#83)
* docs(upstream): triage batch — #739 cherry-pick, 10 issue updates

* feat: add `open_split` and `toggle_split` API

Problem: canola had no way to open a browser in a normal split window;
only floating windows were supported via `open_float`/`toggle_float`.
`M.close` also crashed with E444 when called from the last window.

Solution: port stevearc/oil.nvim#728 — add `open_split(dir, opts, cb)`
and `toggle_split(dir, opts, cb)` mirroring the float API. Use
`is_canola_win`/`canola_original_win` window vars (not the upstream
`is_oil_win` names). Wrap `nvim_win_close` in `pcall` with `enew()`
fallback to handle the last-window E444 case.

Based on: stevearc/oil.nvim#728

* docs: add vimdoc for `open_split`/`toggle_split` and macOS trash recipe

Cherry-picked from: stevearc/oil.nvim#739

* docs(upstream): fix prettier formatting
2026-03-07 15:45:23 -05:00
Barrett Ruth
6d19b5c8f5
correct canola q&a format 2026-03-06 17:58:41 -05:00
Barrett Ruth
5b74210894
docs(upstream): mark #615 fixed, #650 and #682 resolved (#80) 2026-03-06 16:43:55 -05:00
Barrett Ruth
01f10e1d79
refactor: drop nvim 0.8/0.9 compat shims from init.lua (#79) 2026-03-06 16:36:37 -05:00
Barrett Ruth
0f386bb69c
fix: show float title when border is nil (#78)
* fix: show float title when border is nil

Problem: the float title was only shown via the native `nvim_win_set_config`
path, which requires a border to render. The guard `config.float.border ~=
'none'` did not account for `nil`, which is the default — so users with no
explicit `border` config never saw the path title in the floating window.

Solution: require both `~= nil` and `~= 'none'` before using the native
title. In all other cases (border nil, 'none', or nvim < 0.9), fall back to
`util.add_title_to_win`, which renders a child floating window for the title.

Based on: stevearc/oil.nvim#683

* refactor: drop nvim-0.9 version checks in float title logic
2026-03-06 16:29:47 -05:00
Barrett Ruth
ba49f76e91
feat: add skip_confirm_for_delete option (#77)
feat: add \`skip_confirm_for_delete\` option

Problem: there was no way to suppress the confirmation popup when the
only pending operations are deletes. \`skip_confirm_for_simple_edits\`
explicitly excludes deletes, so users who delete frequently had no opt-out.

Solution: add \`skip_confirm_for_delete = false\` config option. When true,
\`confirmation.show()\` skips the popup if every pending action is a delete.

Based on: stevearc/oil.nvim#392
2026-03-06 16:29:12 -05:00
Barrett Ruth
7a46246062
fix: escape on save prompt cancels select (#76)
Problem: when `prompt_save_on_select_new_entry` is enabled and the user
presses Escape on the "Save changes?" confirm dialog, `vim.fn.confirm`
returns 0, but the select continued as if the user had chosen "No".

Solution: add an explicit `choice == 0` branch that returns immediately,
aborting the select without saving or opening any files.
2026-03-06 16:28:57 -05:00
Barrett Ruth
a74747e1f5
feat: emit CanolaFileCreated autocmd on file creation (#75)
* feat: emit \`CanolaFileCreated\` autocmd on file creation

Problem: no way to hook into individual file creation to populate
initial contents, without a plugin-specific config callback.

Solution: fire \`User CanolaFileCreated\` with \`data.path\` after each
successful \`fs.touch\` in the files adapter. Users listen with
\`nvim_create_autocmd\` and write to the path however they like.

* build: gitignore `doc/upstream.html`

* docs(upstream): mark #721 fixed, triage #735

* docs(upstream): simplify #735 note
2026-03-06 15:54:01 -05:00
Barrett Ruth
c7a55fd787
docs(upstream): triage PRs #721 and #735 (#74)
* docs(upstream): triage PRs #721 and #735

* docs(upstream): fix #721 status to deferred

* docs(upstream): remove status key legend

* docs(upstream): s/addressing/fixing in #721 note
2026-03-06 15:48:07 -05:00
Barrett Ruth
1ee6c6b259
feat: add cleanup_buffers_on_delete option (#73)
Problem: When files are deleted via canola, any open Neovim buffers
for those files remain alive, polluting the jumplist with stale
entries.

Solution: Add an opt-in `cleanup_buffers_on_delete` config option
(default `false`). When enabled, `finish()` in `mutator/init.lua`
iterates completed delete actions and wipes matching buffers via
`nvim_buf_delete` before `CanolaActionsPost` fires. Only local
filesystem deletes are handled (guarded by the `files` adapter
check).
2026-03-06 15:19:32 -05:00
Barrett Ruth
69d85b8de1
feat(view): position cursor at name column on new empty lines (#72)
Problem: pressing `o`/`O` in a canola buffer placed the cursor at
column 0, requiring manual navigation past concealed ID prefixes and
column text (icons, permissions) to reach the name column.

Solution: add `show_insert_guide()` which temporarily sets
`virtualedit=all` on empty lines and positions the cursor at the
name column. Computes the correct virtual column by measuring the
visible column prefix width via `nvim_strwidth`, adjusting for
`conceallevel` (0=full ID width, 1=replacement char, 2/3=hidden).
Restores `virtualedit` on `TextChangedI` or `InsertLeave`.
2026-03-06 14:40:10 -05:00
Barrett Ruth
41f375ee9e
fix: restore buflisted on jumplist buffer re-entry (#71)
* fix: restore `buflisted` on jumplist buffer re-entry

Problem: Neovim's jumplist machinery re-enters canola buffers via an
internal `:edit`-equivalent path, which unconditionally sets
`buflisted = true`. The existing workaround in `open()` and
`open_float()` only covers canola-initiated navigation, leaving
`<C-o>` and `<C-i>` unhandled.

Solution: Apply the same `buf_options.buflisted` guard in the
`BufEnter` autocmd, directly after `set_win_options()`. This fires
on every buffer entry — including all jumplist paths — and mirrors
the pattern already used at the two `:edit` callsites.

* docs: mark upstream #302 as fixed in tracker
2026-03-06 11:55:37 -05:00
c3de0004d1
ci: format 2026-03-05 14:53:30 -05:00
Barrett Ruth
0d3088f57e
refactor: rename oil to canola across entire codebase (#70)
Problem: the codebase still used the upstream \`oil\` naming everywhere —
URL schemes, the \`:Oil\` command, highlight groups, user events, module
paths, filetypes, buffer/window variables, LuaCATS type annotations,
vimdoc help tags, syntax groups, and internal identifiers.

Solution: mechanical rename of every reference. URL schemes now use
\`canola://\` (plus \`canola-ssh://\`, \`canola-s3://\`, \`canola-sss://\`,
\`canola-trash://\`, \`canola-test://\`). The \`:Canola\` command replaces
\`:Oil\`. All highlight groups, user events, augroups, namespaces,
filetypes, require paths, type annotations, help tags, and identifiers
follow suit. The \`upstream\` remote to \`stevearc/oil.nvim\` has been
removed and the \`vim.g.oil\` deprecation shim dropped.
2026-03-05 14:50:10 -05:00
Barrett Ruth
67ad0632a6
Remove acknowledgements section from README
Removed acknowledgements for canola.nvim and its maintainers.
2026-03-05 13:45:12 -05:00
Barrett Ruth
c96dbf8d46
Ci/digest final (#69)
* ci(digest): approve with DIGEST_PAT after disabling require_last_push_approval

require_last_push_approval blocked barrettruth from approving their
own push. Disabled that restriction in the ruleset — 1 approval is
still required for all PRs, but the approver can now be the pusher.
DIGEST_PAT (barrettruth) approves, CI runs via PAT push, auto-merge
fires when checks pass.

* ci: format + scripts

* ci: nix
2026-03-04 14:10:07 -05:00
Barrett Ruth
aee5ea10c6
ci: scripts and format (#68)
* ci(digest): approve with DIGEST_PAT after disabling require_last_push_approval

require_last_push_approval blocked barrettruth from approving their
own push. Disabled that restriction in the ruleset — 1 approval is
still required for all PRs, but the approver can now be the pusher.
DIGEST_PAT (barrettruth) approves, CI runs via PAT push, auto-merge
fires when checks pass.

* ci: format + scripts
2026-03-04 13:49:06 -05:00
github-actions[bot]
9b656387fb
docs(upstream): upstream digest (#67)
docs(upstream): upstream digest 2026-03-03

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-03 21:08:58 +00:00
Barrett Ruth
ad03b3771a
ci(digest): approve with DIGEST_PAT after disabling require_last_push_approval (#66)
require_last_push_approval blocked barrettruth from approving their
own push. Disabled that restriction in the ruleset — 1 approval is
still required for all PRs, but the approver can now be the pusher.
DIGEST_PAT (barrettruth) approves, CI runs via PAT push, auto-merge
fires when checks pass.
2026-03-03 16:07:53 -05:00
Barrett Ruth
244db7531c
ci(digest): drop explicit approve, rely on admin bypass (#64)
The GITHUB_TOKEN has admin-level bypass on the ruleset. When
gh pr merge --auto is called, the bypass satisfies the review
requirement automatically — no explicit approve step needed.
The self-review error is gone. PAT still handles the push so
CI triggers.
2026-03-03 16:01:15 -05:00
Barrett Ruth
6af0172eb3
ci(digest): approve with GITHUB_TOKEN not PAT (#61)
require_last_push_approval blocks barrettruth from approving their
own push. The bot (GITHUB_TOKEN) approves instead — different actor
from the PAT pusher, satisfying the rule.
2026-03-03 15:52:27 -05:00
Barrett Ruth
22d9f521d7
ci(digest): unset checkout extraheader so PAT push triggers CI (#59)
Problem: actions/checkout sets an http.extraheader with GITHUB_TOKEN
that overrides any credentials in the remote URL, so git push uses
GITHUB_TOKEN regardless of the URL — suppressing CI triggers.

Solution: unset the extraheader before pushing, forcing git to use
the DIGEST_PAT embedded in the remote URL.
2026-03-03 15:48:18 -05:00
Barrett Ruth
280b3f0f62
ci(digest): push with PAT to trigger CI and auto-approve as barrettruth (#56)
ci(digest): push branch with PAT so CI triggers

Problem: GITHUB_TOKEN suppresses all downstream workflow triggers
including push events, so CI never runs on the digest branch.

Solution: push with DIGEST_PAT (triggers CI as a real user push),
then reset the remote to GITHUB_TOKEN for PR creation. Admin bypass
on the ruleset handles the review requirement.
2026-03-03 15:43:49 -05:00
Barrett Ruth
9ad67b05a6
ci(digest): run CI on push to ci/upstream-digest branch (#53)
Problem: GITHUB_TOKEN-created PRs suppress pull_request triggers,
so CI never runs and auto-merge stalls.

Solution: add ci/upstream-digest to the push trigger in test and
quality workflows. CI runs on the branch push before the PR exists;
check results attach to the commit SHA so the PR sees them as
passing. The digest workflow reverts to GITHUB_TOKEN for PR
creation — no PAT needed, no contribution inflation.
2026-03-03 15:35:25 -05:00
Barrett Ruth
0c930bda2b
ci(digest): create digest PR with PAT so CI triggers (#50)
Problem: GITHUB_TOKEN-created PRs suppress pull_request workflow
triggers, so CI never runs and auto-merge stalls indefinitely.

Solution: use DIGEST_PAT to create the PR. A PAT-created PR is
treated as a real user action, triggering CI normally. Auto-approve
handles the review requirement, auto-merge fires when checks pass.
2026-03-03 15:29:03 -05:00
Barrett Ruth
b7c65a1d4b
ci(digest): remove PAT approval step (#48)
ci(digest): remove PAT approval step — auto-approve handles it
2026-03-03 15:22:20 -05:00
5025803324
docs(upstream): remove #735 and #736 for digest re-test 2026-03-03 15:19:25 -05:00
Barrett Ruth
71b51746af
ci(digest): auto-approve digest PRs via PAT to satisfy review requirement (#46)
Problem: the main branch ruleset requires 1 approving review, which
blocks auto-merge. The GITHUB_TOKEN cannot approve its own PR.

Solution: after creating the PR, approve it using DIGEST_PAT (a
fine-grained PAT stored as a repo secret), then enable auto-merge.
The approval comes from a different actor than the bot, satisfying
require_last_push_approval.
2026-03-03 15:18:20 -05:00
github-actions[bot]
09acf0c3fe
docs(upstream): upstream digest (#45)
docs(upstream): upstream digest 2026-03-03

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-03 15:08:41 -05:00
Barrett Ruth
20bb43057e
ci(digest): use fixed branch with force-push for weekly digest (#44)
Problem: the workflow creates a new dated branch each run. If a digest
PR is not merged before the next run, duplicate PRs accumulate.

Solution: use a single canonical branch ci/upstream-digest with
--force push. Each run resets to main, applies any new items, and
force-pushes. If a PR is already open for the branch, GitHub updates
it in place. A new PR is only created (with auto-merge) when none
exists. The close-stale step is no longer needed.
2026-03-03 15:06:08 -05:00
Barrett Ruth
61e84bbc5f
ci(digest): close existing open digest PRs before creating new one (#42)
Problem: if a digest PR is not merged before the next weekly run, a
second PR is created for the same items plus any new ones, leading
to duplicate open PRs.

Solution: before fetching upstream activity, close any open PRs
labeled upstream/digest (deleting their branches). The new run
re-fetches all items since the last merged baseline and produces a
single up-to-date PR.
2026-03-03 14:59:08 -05:00
Barrett Ruth
56d1745415
ci(digest): update to PR-based upstream tracker workflow (#40) 2026-03-03 14:49:30 -05:00
Barrett Ruth
c4d827070e
ci: add weekly upstream digest workflow (#37)
Problem: new upstream issues and PRs slip through because there's no
mechanism to surface them — manual polling of stevearc/oil.nvim is
required and easy to forget.

Solution: add a Monday 9am UTC scheduled workflow that reads the
highest stevearc/oil.nvim number from doc/upstream.md, fetches merged
PRs and new open issues/PRs above that threshold via the gh CLI, and
creates a structured digest issue in barrettruth/canola.nvim. No issue
is created when there's nothing new. Falls back to a 30-day window if
doc/upstream.md can't be parsed.
2026-03-03 14:15:46 -05:00
63fb912d52
fix(icons): use nonicons hl groups 2026-03-02 20:21:47 -05:00
05234a67ba
fix: use guard clause 2026-03-02 19:26:02 -05:00
262bf8710e
fix: ensure nvim-web-devicoins exists 2026-03-02 19:25:08 -05:00
e90508c459
ci: add bit luajit global 2026-02-23 18:18:35 -05:00
3140c152ea
ci: migrate to nix 2026-02-23 18:13:51 -05:00
b87c665ccb
fix(icon): use fill directory by default 2026-02-23 17:20:44 -05:00
60bfbe05da fix: preserve devicons highlight groups in nonicons icon provider
Problem: when the nonicons direct API was detected, all icons were
returned with the generic 'OilFileIcon' highlight group, losing
per-filetype colors from nvim-web-devicons.

Solution: resolve highlight groups from devicons when available so
nonicons glyphs retain their per-filetype colors.
2026-02-23 15:16:25 -05:00
c51e3168de
fix(dic): format 2026-02-22 22:09:02 -05:00
1fda80f0b2
fix(doc): improve phrasing 2026-02-22 22:07:07 -05:00
9316524fab
fix(doc): credit stevearc 2026-02-22 22:06:54 -05:00
62c9cff67c
fix(doc): readme 2026-02-22 22:05:43 -05:00
eaf4efdf8a
fix(doc): readme 2026-02-22 22:05:41 -05:00
33889b2171
docs: add upstream tracker
Problem: the full upstream PR and issue triage was removed from the
README during the repository modernization, leaving no user-facing
record of what the fork addresses.

Solution: restore the triage tables in a dedicated doc/upstream.md and
link to it from the top of the README. Keeps the README clean while
giving the full picture a permanent home.
2026-02-22 22:04:57 -05:00
c92a5bd42a
docs: rename repository from oil.nvim to canola.nvim
Some checks are pending
luarocks / quality (push) Waiting to run
luarocks / publish (push) Blocked by required conditions
Problem: the fork shared the same name as upstream, making it difficult
to distinguish and discover independently.

Solution: rename the repository to canola.nvim — a type of oil, making
the lineage obvious while establishing a distinct identity. Update all
references in the README, rockspec, and issue templates.
2026-02-22 22:01:34 -05:00
Barrett Ruth
d1f7c691b5
feat(icons): add direct nonicons.nvim icon provider (#31)
Problem: nonicons.nvim exposes a public get_icon/get_icon_by_filetype
API, but oil.nvim can only use nonicons glyphs indirectly through the
devicons monkey-patch. This couples oil to devicons even when nonicons
is available standalone.

Solution: add a nonicons provider in get_icon_provider() between
mini.icons and devicons. Feature-gated on nonicons.get_icon existing
so old nonicons versions fall through to devicons. Uses OilDirIcon
and OilFileIcon highlight groups.
2026-02-22 21:31:09 -05:00
b0f44d3af6
fix: remove nonicons custom impl
Some checks are pending
luarocks / quality (push) Waiting to run
luarocks / publish (push) Blocked by required conditions
2026-02-22 21:03:56 -05:00
Barrett Ruth
07ae3a8dc3
feat(icons): add nonicons.nvim icon provider support (#30)
* feat(icons): add nonicons.nvim icon provider support

Problem: oil.nvim only recognizes mini.icons and nvim-web-devicons as
icon providers. nonicons.nvim works when paired with devicons (via its
apply() monkey-patch), but has no standalone support.

Solution: add a nonicons.nvim fallback in get_icon_provider(), placed
after devicons so the patched devicons path is preferred when both are
installed. The standalone path handles directories via
nonicons.get('file-directory'), files via filetype/extension lookup with
a generic file icon fallback.

* fix(doc): improve readme phrasing
2026-02-22 20:58:22 -05:00
b6aaeaf542
fix(doc): explicitly mention lazy.nvim config 2026-02-22 16:24:42 -05:00
b1b92b6292
fix(doc): readme phrasing 2026-02-22 16:23:02 -05:00
Barrett Ruth
ac787627c0
docs: combine setup and migration FAQ entries (#29) 2026-02-22 16:18:56 -05:00
Barrett Ruth
7d410acaf1
fix(ci): switch typecheck action to stevearc/nvim-typecheck-action (#26)
Problem: mrcjkb/lua-typecheck-action runs lua-language-server in a bare
nix sandbox without neovim installed, causing 71 type errors for all
vim.* and uv.* types. LuaLS 3.17.x also introduced stricter type
checking that flags uv.aliases.fs_types mismatches not present in
3.16.4. The .luarc.json workspace.library entries conflicted with the
action's auto-appended luvit-meta, producing duplicate uv type unions.

Solution: switch to stevearc/nvim-typecheck-action@v2 (matching
upstream), pin LuaLS to 3.16.4, convert .luarc.json to nested format
without workspace.library (let the action provide VIMRUNTIME and
luvit-meta), and add .direnv/* to selene.toml exclude for local use.
2026-02-22 16:14:10 -05:00
Barrett Ruth
b4ab166c39
build: modernize repository (#27)
* build: clean up gitignore and remove empty gitmodules

Problem: .gitignore contained 48 lines of C/shared-object boilerplate
irrelevant to a Lua Neovim plugin. .gitmodules was tracked but empty.

Solution: replace .gitignore with minimal entries covering only files
this project actually produces. Delete the vestigial .gitmodules.

* build: add editorconfig and prettierrc

Problem: no editor or formatter configuration, inconsistent with
cp.nvim and diffs.nvim conventions.

Solution: add .editorconfig (2-space Lua indent, utf-8, final newline)
and .prettierrc (prose wrap, 80 cols, single quotes, no semi) matching
the other repos.

* build: add Makefile for lint and test targets

Problem: .github/pre-commit calls `make fastlint` and .github/pre-push
calls `make lint && make test`, but no Makefile existed, so the git
hooks failed.

Solution: add Makefile with lint (stylua + selene), fastlint
(pre-commit), and test (luarocks test) targets.

* docs: add fork copyright to LICENSE

Problem: LICENSE only contained the original author's copyright notice.

Solution: add a second copyright line for the fork maintainer. MIT
requires retaining the original notice; adding a line for derivative
work is standard practice.

* ci: restructure workflows to quality/test/luarocks pattern

Problem: CI used a single tests.yml for linting, typechecking, and
testing. No conditional path filtering, no markdown format check, and
a stale mirror_upstream_prs.yml and duplicate luarocks.yml existed.

Solution: replace tests.yml with quality.yaml (stylua, selene,
lua-typecheck, prettier with dorny/paths-filter) and test.yaml
(nvim-busted, stable+nightly matrix). Update luarocks.yaml to
reference quality.yaml. Delete mirror_upstream_prs.yml and duplicate
luarocks.yml. Fix automation workflow sender check.

* build: rewrite issue templates

Problem: issue templates used upstream stevearc references, severity
dropdowns, outdated lazy.nvim bootstrap, and the .yml extension
inconsistent with other repos.

Solution: replace with .yaml templates matching cp.nvim/diffs.nvim
style. Bug report uses prerequisites checkboxes, checkhealth output,
modern lazy.nvim bootstrap with vim.g.oil pattern. Feature request
uses problem/solution/alternatives format. Add config.yaml to disable
blank issues and link discussions.

* docs: rewrite README

Problem: README contained upstream triage tables, severity dropdowns,
the old setup() pattern, a tree view question, and references to
stevearc/oil.nvim as the primary source.

Solution: full rewrite matching cp.nvim/diffs.nvim style with bold
tagline, features list, requirements, installation, documentation,
FAQ (lazy.nvim setup with vim.g.oil, migration guide, alternatives),
and acknowledgements crediting the original author.

* revert: remove Makefile

Problem: Makefile was added in b9279b5 but was previously deleted
intentionally.

Solution: remove it.
2026-02-22 16:06:31 -05:00
6f9ed9c7a7
fix(doc): remove A: from q&a section 2026-02-22 15:43:58 -05:00
Barrett Ruth
92ff4d5774
fix(test): resolve busted migration test isolation issues (#25)
* fix(test): resolve busted migration test isolation issues

Problem: Two issues introduced during the plenary-to-busted migration
(6be0148). First, altbuf_spec waited for two BufEnter events but
oil:// → file resolution only fires one async BufEnter (the synchronous
one from vim.cmd.edit fires before wait_for_autocmd is registered).
Second, reset_editor kept the first window which could be a preview
window with stale oil_preview/oil_source_win state, causing
close_preview_window_if_not_in_oil to close the wrong window in
subsequent tests.

Solution: Wait for a single BufEnter in the altbuf test. Replace the
window-keeping logic in reset_editor with vim.cmd.new() +
vim.cmd.only() to guarantee a fresh window with no inherited state.
This also fixes the preview_spec.lua:30 timeout which had the same
root cause. 114/114 tests pass.

* fix(ci): fix nightly tests and remove unused Makefile

Problem: Neovim nightly's ftplugin/markdown.lua now calls
vim.treesitter.start() unconditionally, which crashes in CI where the
markdown parser is not installed. The Makefile is unused — CI runs
selene, stylua, and nvim-busted-action directly.

Solution: Change filetype plugin indent on to filetype on in
minimal_init.lua. Tests only need filetype detection for
vim.bo.filetype assertions, not ftplugin or indent loading. Remove
the Makefile.
2026-02-22 15:39:20 -05:00
36cc369de3
fix(ci): set ft plugin indent 2026-02-22 00:32:41 -05:00
6be0148eef
build: migrate test framework from plenary to busted
Problem: plenary.nvim is deprecated. The test suite depends on
plenary's async test runner and coroutine-based utilities, tying the
project to an unmaintained dependency. CI also tests against Neovim
0.8-0.11, which are no longer relevant.

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

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

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

* build: add nix devshell and pre-commit hooks

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

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

* fix: format with stylua

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

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

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

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

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

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

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

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

9
.busted Normal file
View file

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

9
.editorconfig Normal file
View file

@ -0,0 +1,9 @@
root = true
[*]
insert_final_newline = true
charset = utf-8
[*.lua]
indent_style = space
indent_size = 2

3
.envrc
View file

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

80
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View file

@ -0,0 +1,80 @@
name: Bug Report
description: Report a bug
title: 'bug: '
labels: [bug]
body:
- type: checkboxes
attributes:
label: Prerequisites
options:
- label:
I have searched [existing
issues](https://github.com/barrettruth/canola.nvim/issues)
required: true
- label: I have updated to the latest version
required: true
- type: textarea
attributes:
label: 'Neovim version'
description: 'Output of `nvim --version`'
render: text
validations:
required: true
- type: input
attributes:
label: 'Operating system'
placeholder: 'e.g. Arch Linux, macOS 15, Ubuntu 24.04'
validations:
required: true
- type: textarea
attributes:
label: Description
description: What happened? What did you expect?
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: Minimal steps to trigger the bug
value: |
1.
2.
3.
validations:
required: true
- type: textarea
attributes:
label: 'Health check'
description: 'Output of `:checkhealth canola`'
render: text
- type: textarea
attributes:
label: Minimal reproduction
description: |
Save the script below as `repro.lua`, edit if needed, and run:
```
nvim -u repro.lua
```
Confirm the bug reproduces with this config before submitting.
render: lua
value: |
vim.env.LAZY_STDPATH = '.repro'
load(vim.fn.system('curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua'))()
require('lazy.nvim').setup({
spec = {
{
'barrettruth/canola.nvim',
init = function()
vim.g.canola = {}
end,
},
},
})
validations:
required: true

View file

@ -1,131 +0,0 @@
name: Bug Report
description: File a bug/issue
title: "bug: "
labels: [bug]
body:
- type: markdown
attributes:
value: |
Before reporting a bug, make sure to search [existing issues](https://github.com/stevearc/oil.nvim/issues)
- type: checkboxes
attributes:
label: Did you check the docs and existing issues?
options:
- label: I have read the docs
required: true
- label: I have searched the existing issues
required: true
- type: input
attributes:
label: "Neovim version (nvim -v)"
placeholder: "0.8.0 commit db1b0ee3b30f"
validations:
required: true
- type: input
attributes:
label: "Operating system/version"
placeholder: "MacOS 11.5"
validations:
required: true
- type: textarea
attributes:
label: Describe the bug
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
description: Steps to reproduce the behavior.
placeholder: |
1. nvim -u repro.lua
2.
3.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Directory structure
description: The structure of the directory used to reproduce the bug
placeholder: |
a/b/foo.txt
a/bar.md
a/c/baz.txt
validations:
required: false
- type: textarea
attributes:
label: Repro
description:
Minimal `init.lua` to reproduce this issue. Save as `repro.lua` and run with `nvim -u repro.lua`
This uses lazy.nvim (a plugin manager).
You can add your config with the `config` key the same way you can do with packer.nvim.
value: |
-- save as repro.lua
-- run with nvim -u repro.lua
-- DO NOT change the paths
local root = vim.fn.fnamemodify("./.repro", ":p")
-- set stdpaths to use .repro
for _, name in ipairs({ "config", "data", "state", "runtime", "cache" }) do
vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end
-- bootstrap lazy
local lazypath = root .. "/plugins/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"--single-branch",
"https://github.com/folke/lazy.nvim.git",
lazypath,
})
end
vim.opt.runtimepath:prepend(lazypath)
-- install plugins
local plugins = {
"folke/tokyonight.nvim",
{
"stevearc/oil.nvim",
config = function()
require("oil").setup({
-- add any needed settings here
})
end,
},
-- add any other plugins here
}
require("lazy").setup(plugins, {
root = root .. "/plugins",
})
vim.cmd.colorscheme("tokyonight")
-- add anything else here
render: Lua
validations:
required: true
- type: checkboxes
attributes:
label: Did you check the bug with a clean config?
options:
- label: I have confirmed that the bug reproduces with `nvim -u repro.lua` using the repro.lua file above.
required: true

5
.github/ISSUE_TEMPLATE/config.yaml vendored Normal file
View file

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions
url: https://github.com/barrettruth/canola.nvim/discussions
about: Ask questions and discuss ideas

View file

@ -0,0 +1,30 @@
name: Feature Request
description: Suggest a feature
title: 'feat: '
labels: [enhancement]
body:
- type: checkboxes
attributes:
label: Prerequisites
options:
- label:
I have searched [existing
issues](https://github.com/barrettruth/canola.nvim/issues)
required: true
- type: textarea
attributes:
label: Problem
description: What problem does this solve?
validations:
required: true
- type: textarea
attributes:
label: Proposed solution
validations:
required: true
- type: textarea
attributes:
label: Alternatives considered

View file

@ -1,43 +0,0 @@
name: Feature Request
description: Submit a feature request
title: "feature request: "
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Before submitting a feature request, make sure to search for [existing requests](https://github.com/stevearc/oil.nvim/issues)
- type: checkboxes
attributes:
label: Did you check existing requests?
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Describe the feature
description: A short summary of the feature you want
validations:
required: true
- type: textarea
attributes:
label: Provide background
description: Describe the reasoning behind why you want the feature.
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
description: Any additional information you would like to provide. Things you've tried, alternatives considered, examples from other plugins, etc.
validations:
required: false

158
.github/scripts/upstream_digest.py vendored Normal file
View file

@ -0,0 +1,158 @@
#!/usr/bin/env python3
import json
import os
import re
import subprocess
import sys
from datetime import date, timedelta
UPSTREAM = "stevearc/oil.nvim"
UPSTREAM_MD = "doc/upstream.md"
PRS_HEADING = "## Upstream PRs"
ISSUES_HEADING = "## Issues"
def get_last_tracked_number():
try:
with open(UPSTREAM_MD) as f:
content = f.read()
numbers = re.findall(
r"\[#(\d+)\]\(https://github\.com/stevearc/oil\.nvim", content
)
if numbers:
return max(int(n) for n in numbers)
except OSError:
pass
return None
def gh(*args):
result = subprocess.run(
["gh"] + list(args),
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
def fetch_items(last_number, since_date):
merged_prs = json.loads(
gh(
"pr", "list",
"--repo", UPSTREAM,
"--state", "merged",
"--limit", "100",
"--json", "number,title,url",
)
)
open_prs = json.loads(
gh(
"pr", "list",
"--repo", UPSTREAM,
"--state", "open",
"--limit", "100",
"--json", "number,title,createdAt,url",
)
)
open_issues = json.loads(
gh(
"issue", "list",
"--repo", UPSTREAM,
"--state", "open",
"--limit", "100",
"--json", "number,title,createdAt,url",
)
)
if last_number is not None:
merged_prs = [x for x in merged_prs if x["number"] > last_number]
open_prs = [x for x in open_prs if x["number"] > last_number]
open_issues = [x for x in open_issues if x["number"] > last_number]
else:
cutoff = since_date.isoformat()
merged_prs = []
open_prs = [x for x in open_prs if x.get("createdAt", "") >= cutoff]
open_issues = [x for x in open_issues if x.get("createdAt", "") >= cutoff]
merged_prs.sort(key=lambda x: x["number"])
open_prs.sort(key=lambda x: x["number"])
open_issues.sort(key=lambda x: x["number"])
return merged_prs, open_prs, open_issues
def append_to_section(content, heading, new_rows):
lines = content.split("\n")
in_section = False
last_table_row = -1
for i, line in enumerate(lines):
if line.startswith("## "):
in_section = line.strip() == heading
if in_section and line.startswith("|"):
last_table_row = i
if last_table_row == -1:
return content
return "\n".join(
lines[: last_table_row + 1] + new_rows + lines[last_table_row + 1 :]
)
def main():
last_number = get_last_tracked_number()
since_date = date.today() - timedelta(days=30)
merged_prs, open_prs, open_issues = fetch_items(last_number, since_date)
total = len(merged_prs) + len(open_prs) + len(open_issues)
if total == 0:
print("No new upstream activity.")
return
with open(UPSTREAM_MD) as f:
content = f.read()
pr_rows = []
for pr in open_prs:
pr_rows.append(f"| [#{pr['number']}]({pr['url']}) | {pr['title']} | open |")
for pr in merged_prs:
pr_rows.append(
f"| [#{pr['number']}]({pr['url']}) | {pr['title']} | merged — not cherry-picked |"
)
issue_rows = []
for issue in open_issues:
issue_rows.append(
f"| [#{issue['number']}]({issue['url']}) | {issue['title']} | open |"
)
if pr_rows:
content = append_to_section(content, PRS_HEADING, pr_rows)
if issue_rows:
content = append_to_section(content, ISSUES_HEADING, issue_rows)
with open(UPSTREAM_MD, "w") as f:
f.write(content)
github_output = os.environ.get("GITHUB_OUTPUT")
if github_output:
with open(github_output, "a") as f:
f.write("changed=true\n")
print(
f"Added {len(open_prs)} open PR(s), {len(merged_prs)} merged PR(s), "
f"{len(open_issues)} issue(s) to {UPSTREAM_MD}"
)
if __name__ == "__main__":
try:
main()
except subprocess.CalledProcessError as e:
print(f"gh command failed: {e.stderr}", file=sys.stderr)
sys.exit(1)

View file

@ -8,7 +8,7 @@ jobs:
# issues in my "needs triage" filter.
remove_question:
runs-on: ubuntu-latest
if: github.event.sender.login != 'stevearc'
if: github.event.sender.login != 'barrettruth'
steps:
- uses: actions/checkout@v4
- uses: actions-ecosystem/action-remove-labels@v1

View file

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

View file

@ -7,7 +7,7 @@ on:
jobs:
quality:
uses: ./.github/workflows/tests.yml
uses: ./.github/workflows/quality.yaml
publish:
needs: quality

View file

@ -1,85 +0,0 @@
name: Mirror Upstream PRs
on:
schedule:
- cron: "0 8 * * *"
workflow_dispatch:
permissions:
issues: write
jobs:
mirror:
runs-on: ubuntu-latest
steps:
- name: Mirror new upstream PRs
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const upstream = { owner: 'stevearc', repo: 'oil.nvim' };
const fork = { owner: 'barrettruth', repo: 'oil.nvim' };
const since = new Date();
since.setDate(since.getDate() - 1);
const sinceISO = since.toISOString();
const { data: prs } = await github.rest.pulls.list({
...upstream,
state: 'open',
sort: 'created',
direction: 'desc',
per_page: 30,
});
const recentPRs = prs.filter(pr => {
if (new Date(pr.created_at) < since) return false;
if (pr.user.login === 'dependabot[bot]') return false;
if (pr.user.login === 'dependabot-preview[bot]') return false;
return true;
});
if (recentPRs.length === 0) {
console.log('No new upstream PRs in the last 24 hours.');
return;
}
const { data: existingIssues } = await github.rest.issues.listForRepo({
...fork,
state: 'all',
labels: 'upstream/pr',
per_page: 100,
});
const mirroredNumbers = new Set();
for (const issue of existingIssues) {
const match = issue.title.match(/^upstream#(\d+)/);
if (match) mirroredNumbers.add(parseInt(match[1]));
}
for (const pr of recentPRs) {
if (mirroredNumbers.has(pr.number)) {
console.log(`Skipping PR #${pr.number} — already mirrored.`);
continue;
}
const labels = pr.labels.map(l => l.name).join(', ') || 'none';
await github.rest.issues.create({
...fork,
title: `upstream#${pr.number}: ${pr.title}`,
body: [
`Mirrored from [stevearc/oil.nvim#${pr.number}](${pr.html_url}).`,
'',
`**Author:** @${pr.user.login}`,
`**Labels:** ${labels}`,
`**Created:** ${pr.created_at}`,
'',
'---',
'',
pr.body || '*No description provided.*',
].join('\n'),
labels: ['upstream/pr'],
});
console.log(`Created issue for upstream PR #${pr.number}: ${pr.title}`);
}

73
.github/workflows/quality.yaml vendored Normal file
View file

@ -0,0 +1,73 @@
name: quality
on:
workflow_call:
pull_request:
branches: [main]
push:
branches: [main, ci/upstream-digest]
jobs:
changes:
runs-on: ubuntu-latest
outputs:
lua: ${{ steps.changes.outputs.lua }}
markdown: ${{ steps.changes.outputs.markdown }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
lua:
- 'lua/**'
- 'plugin/**'
- 'spec/**'
- '*.lua'
- '.luarc.json'
- '*.toml'
- 'vim.yaml'
markdown:
- '*.md'
lua-format:
name: Lua Format Check
runs-on: ubuntu-latest
needs: changes
if: ${{ needs.changes.outputs.lua == 'true' }}
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v31
- run: nix develop --command stylua --check lua spec
lua-lint:
name: Lua Lint Check
runs-on: ubuntu-latest
needs: changes
if: ${{ needs.changes.outputs.lua == 'true' }}
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v31
- run: nix develop --command selene --display-style quiet .
lua-typecheck:
name: Lua Type Check
runs-on: ubuntu-latest
needs: changes
if: ${{ needs.changes.outputs.lua == 'true' }}
steps:
- uses: actions/checkout@v4
- uses: stevearc/nvim-typecheck-action@v2
with:
path: lua
luals-version: 3.16.4
markdown-format:
name: Markdown Format Check
runs-on: ubuntu-latest
needs: changes
if: ${{ needs.changes.outputs.markdown == 'true' }}
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v31
- run: nix develop --command prettier --check .

22
.github/workflows/test.yaml vendored Normal file
View file

@ -0,0 +1,22 @@
name: test
on:
pull_request:
branches: [main]
push:
branches: [main, ci/upstream-digest]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
nvim: [stable, nightly]
name: Test (Neovim ${{ matrix.nvim }})
steps:
- uses: actions/checkout@v4
- uses: nvim-neorocks/nvim-busted-action@v1
with:
nvim_version: ${{ matrix.nvim }}

View file

@ -1,72 +0,0 @@
name: Tests
on:
workflow_call:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
luacheck:
name: Luacheck
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Prepare
run: |
sudo apt-get update
sudo add-apt-repository universe
sudo apt install luarocks -y
sudo luarocks install luacheck
- name: Run Luacheck
run: luacheck lua tests
stylua:
name: StyLua
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Stylua
uses: JohnnyMorganz/stylua-action@v4
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
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
name: Run tests
runs-on: ubuntu-22.04
env:
NVIM_TAG: ${{ matrix.nvim_tag }}
steps:
- uses: actions/checkout@v4
- name: Install Neovim and dependencies
run: |
bash ./.github/workflows/install_nvim.sh
- name: Run tests
run: |
bash ./run_tests.sh

50
.github/workflows/upstream-digest.yaml vendored Normal file
View file

@ -0,0 +1,50 @@
name: upstream digest
on:
schedule:
- cron: '0 9 * * 1'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
digest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Update upstream tracker
id: digest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 .github/scripts/upstream_digest.py
- name: Format doc/upstream.md
if: steps.digest.outputs.changed == 'true'
run: npx --yes prettier --write doc/upstream.md
- name: Push and open PR if needed
if: steps.digest.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BRANCH="ci/upstream-digest"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "${BRANCH}"
git add doc/upstream.md
git commit -m "docs(upstream): upstream digest $(date +%Y-%m-%d)"
git config --unset http.https://github.com/.extraheader
git remote set-url origin "https://x-access-token:${{ secrets.DIGEST_PAT }}@github.com/barrettruth/canola.nvim.git"
git push --force origin "${BRANCH}"
if ! GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" gh pr list --head "${BRANCH}" --state open --json number --jq '.[0].number' | grep -q .; then
PR_URL=$(GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" gh pr create \
--title "docs(upstream): upstream digest" \
--body "Automated weekly digest of new upstream activity. Triage by updating statuses and notes." \
--base main \
--head "${BRANCH}")
GH_TOKEN="${{ secrets.DIGEST_PAT }}" gh pr review "${PR_URL}" --approve
gh pr merge "${PR_URL}" --auto --squash
fi

56
.gitignore vendored
View file

@ -1,48 +1,14 @@
# Compiled Lua sources
luac.out
# luarocks build files
*.src.rock
*.zip
*.tar.gz
# Object files
*.o
*.os
*.ko
*.obj
*.elf
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
*.def
*.exp
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
.direnv/
.testenv/
doc/tags
scripts/nvim-typecheck-action
scripts/benchmark.nvim
doc/upstream.html
*.log
.*cache*
CLAUDE.md
.claude/
node_modules/
.direnv/
.envrc
venv/
perf/tmp/
scripts/benchmark.nvim
profile.json
.worktrees/

0
.gitmodules vendored
View file

View file

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

View file

@ -3,7 +3,14 @@
"version": "LuaJIT",
"pathStrict": true
},
"workspace": {
"checkThirdParty": false,
"ignoreDir": [".direnv"]
},
"type": {
"checkTableShape": true
},
"completion": {
"callSnippet": "Replace"
}
}

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

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

9
.prettierrc Normal file
View file

@ -0,0 +1,9 @@
{
"proseWrap": "always",
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "none",
"semi": false,
"singleQuote": true
}

View file

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

1
.styluaignore Normal file
View file

@ -0,0 +1 @@
.direnv/

View file

@ -1,6 +1,7 @@
MIT License
Copyright (c) 2022 Steven Arcangeli
Copyright (c) 2025 Barrett Ruth
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,48 +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: lint and run tests
.PHONY: all
all: lint test
## test: run tests
.PHONY: test
test:
./run_tests.sh
## lint: run linters and LuaLS typechecking
.PHONY: lint
lint: scripts/nvim-typecheck-action
./scripts/nvim-typecheck-action/typecheck.sh --workdir scripts/nvim-typecheck-action lua
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-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-typecheck-action venv .testenv perf/tmp profile.json

174
README.md
View file

@ -1,145 +1,109 @@
# oil.nvim
# canola.nvim
A [vim-vinegar](https://github.com/tpope/vim-vinegar) like file explorer that lets you edit your filesystem like a normal Neovim buffer.
A refined [oil.nvim](https://github.com/stevearc/oil.nvim) — edit your
filesystem like a buffer, with bug fixes and community PRs that haven't landed
upstream.
[Upstream tracker](doc/upstream.md) — full PR and issue triage against
[oil.nvim](https://github.com/stevearc/oil.nvim)
https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-9461-80e9717cea94.mp4
This is a maintained fork of [stevearc/oil.nvim](https://github.com/stevearc/oil.nvim)
with cherry-picked upstream PRs and original bug fixes that haven't landed
upstream yet.
## Features
<details>
<summary>Changes from upstream</summary>
### PRs
Upstream PRs cherry-picked or adapted into this fork.
| PR | Description | Commit |
|---|---|---|
| [#495](https://github.com/stevearc/oil.nvim/pull/495) | Cancel visual/operator-pending mode on close instead of closing buffer | [`16f3d7b`](https://github.com/barrettruth/oil.nvim/commit/16f3d7b) |
| [#537](https://github.com/stevearc/oil.nvim/pull/537) | Configurable file and directory creation permissions (`new_file_mode`, `new_dir_mode`) | [`c6b4a7a`](https://github.com/barrettruth/oil.nvim/commit/c6b4a7a) |
| [#578](https://github.com/stevearc/oil.nvim/issues/578) | Recipe to disable hidden file dimming by relinking `Oil*Hidden` groups | [`38db6cf`](https://github.com/barrettruth/oil.nvim/commit/38db6cf) |
| [#618](https://github.com/stevearc/oil.nvim/pull/618) | Opt-in filetype detection for icons via `use_slow_filetype_detection` | [`ded1725`](https://github.com/barrettruth/oil.nvim/commit/ded1725) |
| [#644](https://github.com/stevearc/oil.nvim/pull/644) | Pass full entry to `is_hidden_file` and `is_always_hidden` callbacks | [`4ab4765`](https://github.com/barrettruth/oil.nvim/commit/4ab4765) |
| [#645](https://github.com/stevearc/oil.nvim/pull/645) | Add `close_float` action (close only floating oil windows) | [`f6bcdda`](https://github.com/barrettruth/oil.nvim/commit/f6bcdda) |
| [#690](https://github.com/stevearc/oil.nvim/pull/690) | Add `OilFileIcon` highlight group as fallback for unrecognized icons | [`ce64ae1`](https://github.com/barrettruth/oil.nvim/commit/ce64ae1) |
| [#697](https://github.com/stevearc/oil.nvim/pull/697) | Recipe for custom file extension column with sorting | [`dcb3a08`](https://github.com/barrettruth/oil.nvim/commit/dcb3a08) |
| [#698](https://github.com/stevearc/oil.nvim/pull/698) | Executable file highlighting (`OilExecutable`, `OilExecutableHidden`) | [`41556ec`](https://github.com/barrettruth/oil.nvim/commit/41556ec), [`85ed9b8`](https://github.com/barrettruth/oil.nvim/commit/85ed9b8) |
| [#717](https://github.com/stevearc/oil.nvim/pull/717) | Add malewicz1337/oil-git.nvim to third-party extensions | [`582d9fc`](https://github.com/barrettruth/oil.nvim/commit/582d9fc) |
| [#720](https://github.com/stevearc/oil.nvim/pull/720) | Gate `BufAdd` autocmd behind `default_file_explorer` check | [`2228f80`](https://github.com/barrettruth/oil.nvim/commit/2228f80) |
| [#722](https://github.com/stevearc/oil.nvim/pull/722) | Fix dead freedesktop trash specification URL | [`b92ecb0`](https://github.com/barrettruth/oil.nvim/commit/b92ecb0) |
| [#723](https://github.com/stevearc/oil.nvim/pull/723) | Emit `OilReadPost` user event after every buffer render | [`29239d5`](https://github.com/barrettruth/oil.nvim/commit/29239d5) |
| [#725](https://github.com/stevearc/oil.nvim/pull/725) | Normalize keymap keys before config merge (`<c-t>` = `<C-t>`) | [`723145c`](https://github.com/barrettruth/oil.nvim/commit/723145c) |
| [#727](https://github.com/stevearc/oil.nvim/pull/727) | Clarify `get_current_dir` nil return and add Telescope recipe | [`eed6697`](https://github.com/barrettruth/oil.nvim/commit/eed6697) |
### Issues
Upstream issues triaged against this fork.
| Issue | Status | Resolution |
|---|---|---|
| [#446](https://github.com/stevearc/oil.nvim/issues/446) | resolved | Executable highlighting — implemented by PR [#698](https://github.com/stevearc/oil.nvim/pull/698) |
| [#483](https://github.com/stevearc/oil.nvim/issues/483) | not actionable | Spell downloads depend on netrw — fixed in neovim ([neovim#34940](https://github.com/neovim/neovim/pull/34940)) |
| [#492](https://github.com/stevearc/oil.nvim/issues/492) | not actionable | Question — j/k remapping, answered in comments |
| [#533](https://github.com/stevearc/oil.nvim/issues/533) | not actionable | `constrain_cursor` — needs repro from reporter |
| [#587](https://github.com/stevearc/oil.nvim/issues/587) | not actionable | Alt+h keymap — user config issue |
| [#623](https://github.com/stevearc/oil.nvim/issues/623) | not actionable | bufferline.nvim interaction — cross-plugin issue |
| [#624](https://github.com/stevearc/oil.nvim/issues/624) | not actionable | Mutation-in-progress race — no reliable repro |
| [#632](https://github.com/stevearc/oil.nvim/issues/632) | fixed | Preview + move = copy — [`fe16993`](https://github.com/barrettruth/oil.nvim/commit/fe16993) |
| [#642](https://github.com/stevearc/oil.nvim/issues/642) | fixed | W10 warning under `nvim -R` — [`ca834cf`](https://github.com/barrettruth/oil.nvim/commit/ca834cf) |
| [#664](https://github.com/stevearc/oil.nvim/issues/664) | not actionable | Extra buffer on session reload — no repro |
| [#670](https://github.com/stevearc/oil.nvim/issues/670) | fixed | Multi-directory cmdline — [`70861e5`](https://github.com/barrettruth/oil.nvim/commit/70861e5) |
| [#673](https://github.com/stevearc/oil.nvim/issues/673) | fixed | Symlink newlines crash — [`9110a1a`](https://github.com/barrettruth/oil.nvim/commit/9110a1a) |
| [#679](https://github.com/stevearc/oil.nvim/issues/679) | resolved | Executable file sign — implemented by PR [#698](https://github.com/stevearc/oil.nvim/pull/698) |
| [#692](https://github.com/stevearc/oil.nvim/issues/692) | resolved | Keymap normalization — fixed by PR [#725](https://github.com/stevearc/oil.nvim/pull/725) |
| [#710](https://github.com/stevearc/oil.nvim/issues/710) | fixed | buftype empty on BufEnter — [`01b860e`](https://github.com/barrettruth/oil.nvim/commit/01b860e) |
| [#714](https://github.com/stevearc/oil.nvim/issues/714) | not actionable | Support question — already answered |
| [#719](https://github.com/stevearc/oil.nvim/issues/719) | not actionable | Neovim crash on node_modules delete — libuv/neovim bug |
| [#726](https://github.com/stevearc/oil.nvim/issues/726) | not actionable | Meta discussion/roadmap |
</details>
- Edit directory listings as normal buffers — mutations are derived by diffing
- Cross-directory move, copy, and rename across any adapter
- Adapters for local filesystem, SSH, S3, and OS trash
- File preview in split or floating window
- Configurable columns (icon, size, permissions, timestamps)
- Executable file highlighting and filetype-aware icons
- Floating window and split layouts
## Requirements
Neovim 0.8+ and optionally [mini.icons](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-icons.md) or [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) for file icons.
- Neovim 0.10+
- (Optionally) any of the following icon providers:
- [mini.icons](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-icons.md)
- [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons)
- [nonicons.nvim](https://github.com/barrettruth/nonicons.nvim)
## Installation
Install with your favorite package manager or with luarocks:
Install with your package manager of choice or via
[luarocks](https://luarocks.org/modules/barrettruth/canola.nvim):
```console
luarocks install oil.nvim
```
luarocks install canola.nvim
```
## Documentation
```vim
:help oil.nvim
:help canola.nvim
```
## FAQ
**Q: How do I migrate from `stevearc/oil.nvim` to `barrettruth/oil.nvim`?**
**Q: How do I migrate from `stevearc/oil.nvim`?**
Replace your `setup()` call with a `vim.g.oil` assignment. For example, with
[lazy.nvim](https://github.com/folke/lazy.nvim):
Change the plugin source and replace `setup()` with `vim.g.canola` in `init`.
The configuration table is identical — only the entry point changes. For
example, with [lazy.nvim](https://github.com/folke/lazy.nvim):
Before (`stevearc/oil.nvim`):
```lua
-- before
{
'stevearc/oil.nvim',
opts = { ... },
config = function(_, opts)
require('oil').setup(opts)
end,
opts = {
...
}
}
-- after
{
'barrettruth/oil.nvim',
init = function()
vim.g.oil = {
columns = { "icon", "size" },
delete_to_trash = true,
}
end,
}
```
After (`barrettruth/canola.nvim`):
```lua
{
'barrettruth/canola.nvim',
opts = { ... },
config = function(_, opts)
require('canola').setup(opts)
end,
}
```
`init` runs before the plugin loads; `config` runs after. oil.nvim reads
`vim.g.canola` at load time, so `init` is the correct hook. Do not use `config`,
`opts`, or `lazy` — oil.nvim loads itself when you open a directory.
**Q: Why "canola"?**
Canola oil! But...
**Q: Why "oil"?**
**A:** From the [vim-vinegar](https://github.com/tpope/vim-vinegar) README, a quote by Drew Neil:
From the [vim-vinegar](https://github.com/tpope/vim-vinegar) README, a quote by
Drew Neil:
> Split windows and the project drawer go together like oil and vinegar
Vinegar was taken. Let's be oil.
Plus, I think it's pretty slick ;)
**Q: Why would I want to use oil vs any other plugin?**
**A:**
- 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)
If you don't need those features specifically, check out the alternatives listed below
**Q: Can oil display files as a tree view?**
**A:** No. A tree view would require a completely different methodology, necessitating a complete rewrite.
**Q: What are some alternatives?**
**A:**
- [stevearc/oil.nvim](https://github.com/stevearc/oil.nvim): the original
- [mini.files](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-files.md):
cross-directory filesystem-as-buffer with a column view
- [vim-vinegar](https://github.com/tpope/vim-vinegar): the granddaddy of
single-directory file browsing
- [dirbuf.nvim](https://github.com/elihunter173/dirbuf.nvim): filesystem as
buffer without cross-directory edits
- [lir.nvim](https://github.com/tamago324/lir.nvim): vim-vinegar style with
Neovim integration
- [vim-dirvish](https://github.com/justinmk/vim-dirvish): stable, simple
directory browser
- [the original](https://github.com/stevearc/oil.nvim): the lesser-maintained but
official `oil.nvim`
- [mini.files](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-files.md): Also supports cross-directory filesystem-as-buffer edits with a column view.
- [vim-vinegar](https://github.com/tpope/vim-vinegar): The granddaddy of single-directory file browsing.
- [dirbuf.nvim](https://github.com/elihunter173/dirbuf.nvim): Edit filesystem like a buffer, but no cross-directory edits.
- [lir.nvim](https://github.com/tamago324/lir.nvim): Similar to vim-vinegar with better Neovim integration.
- [vim-dirvish](https://github.com/justinmk/vim-dirvish): Stable, simple directory browser.
## Acknowledgements
- [stevearc](https://github.com/stevearc):
[oil.nvim](https://github.com/stevearc/oil.nvim)

View file

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

File diff suppressed because it is too large Load diff

144
doc/upstream.md Normal file
View file

@ -0,0 +1,144 @@
# Upstream Tracker
Triage of [stevearc/oil.nvim](https://github.com/stevearc/oil.nvim) PRs and
issues against this fork.
## Upstream PRs
| PR | Description | Status |
| ----------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------ |
| [#495](https://github.com/stevearc/oil.nvim/pull/495) | Cancel visual/operator-pending mode on close | cherry-picked |
| [#537](https://github.com/stevearc/oil.nvim/pull/537) | Configurable file/directory creation permissions | cherry-picked |
| [#618](https://github.com/stevearc/oil.nvim/pull/618) | Opt-in filetype detection for icons | cherry-picked |
| [#644](https://github.com/stevearc/oil.nvim/pull/644) | Pass entry to `is_hidden_file`/`is_always_hidden` | cherry-picked |
| [#697](https://github.com/stevearc/oil.nvim/pull/697) | Recipe for file extension column | cherry-picked |
| [#698](https://github.com/stevearc/oil.nvim/pull/698) | Executable file highlighting | cherry-picked |
| [#717](https://github.com/stevearc/oil.nvim/pull/717) | Add oil-git.nvim to extensions | cherry-picked |
| [#720](https://github.com/stevearc/oil.nvim/pull/720) | Gate `BufAdd` autocmd behind config check | cherry-picked |
| [#722](https://github.com/stevearc/oil.nvim/pull/722) | Fix freedesktop trash URL | cherry-picked |
| [#723](https://github.com/stevearc/oil.nvim/pull/723) | Emit `OilReadPost` event after render | cherry-picked |
| [#725](https://github.com/stevearc/oil.nvim/pull/725) | Normalize keymap keys before config merge | cherry-picked |
| [#727](https://github.com/stevearc/oil.nvim/pull/727) | Clarify `get_current_dir` nil + Telescope recipe | cherry-picked |
| [#739](https://github.com/stevearc/oil.nvim/pull/739) | macOS FreeDesktop trash recipe | cherry-picked |
| [#488](https://github.com/stevearc/oil.nvim/pull/488) | Parent directory in a split | not actionable — empty PR |
| [#493](https://github.com/stevearc/oil.nvim/pull/493) | UNC paths on Windows | not actionable — superseded by [#686](https://github.com/stevearc/oil.nvim/pull/686) |
| [#686](https://github.com/stevearc/oil.nvim/pull/686) | Windows path conversion fix | not actionable — Windows-only |
| [#735](https://github.com/stevearc/oil.nvim/pull/735) | gX opens external program with selection | not actionable — hardcoded Linux-only, incomplete |
| [#591](https://github.com/stevearc/oil.nvim/pull/591) | release-please changelog | not applicable |
| [#667](https://github.com/stevearc/oil.nvim/pull/667) | Virtual text columns + headers | deferred — WIP, conflicting |
| [#708](https://github.com/stevearc/oil.nvim/pull/708) | Move file into new dir by renaming | deferred — needs rewrite |
| [#721](https://github.com/stevearc/oil.nvim/pull/721) | `create_hook` to populate file contents | deferred — fixing via autocmd event |
| [#728](https://github.com/stevearc/oil.nvim/pull/728) | `open_split` for opening oil in a split | deferred — tracked as [#2](https://github.com/barrettruth/canola.nvim/issues/2) |
## Issues
| Issue | Description | Status |
| ------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| [#85](https://github.com/stevearc/oil.nvim/issues/85) | Git status column | open |
| [#95](https://github.com/stevearc/oil.nvim/issues/95) | Undo after renaming files | open |
| [#117](https://github.com/stevearc/oil.nvim/issues/117) | Move file into new dir via slash in name | open |
| [#156](https://github.com/stevearc/oil.nvim/issues/156) | Paste path of files into oil buffer | open |
| [#200](https://github.com/stevearc/oil.nvim/issues/200) | Highlights not working when opening a file | open |
| [#207](https://github.com/stevearc/oil.nvim/issues/207) | Suppress "no longer available" message | fixed — `cleanup_buffers_on_delete` option |
| [#210](https://github.com/stevearc/oil.nvim/issues/210) | FTP support | open |
| [#213](https://github.com/stevearc/oil.nvim/issues/213) | Disable preview for large files | fixed ([#85](https://github.com/barrettruth/canola.nvim/pull/85)) |
| [#226](https://github.com/stevearc/oil.nvim/issues/226) | K8s/Docker adapter | not actionable — no demand |
| [#232](https://github.com/stevearc/oil.nvim/issues/232) | Cannot close last window | open |
| [#254](https://github.com/stevearc/oil.nvim/issues/254) | Buffer modified highlight group | open |
| [#263](https://github.com/stevearc/oil.nvim/issues/263) | Diff mode | open |
| [#276](https://github.com/stevearc/oil.nvim/issues/276) | Archives manipulation | not actionable — nvim has builtin zip support |
| [#280](https://github.com/stevearc/oil.nvim/issues/280) | vim-projectionist support | open |
| [#288](https://github.com/stevearc/oil.nvim/issues/288) | No reliable repro; likely lazy.nvim timing | not actionable |
| [#289](https://github.com/stevearc/oil.nvim/issues/289) | Show absolute path toggle | open |
| [#294](https://github.com/stevearc/oil.nvim/issues/294) | Can't handle emojis in filenames | not actionable — libuv bug ([nodejs/node#49042](https://github.com/nodejs/node/issues/49042)) |
| [#298](https://github.com/stevearc/oil.nvim/issues/298) | Open float on neovim directory startup | open |
| [#302](https://github.com/stevearc/oil.nvim/issues/302) | `buflisted=true` after jumplist nav | fixed ([#71](https://github.com/barrettruth/canola.nvim/pull/71)) |
| [#303](https://github.com/stevearc/oil.nvim/issues/303) | Preview in float window mode | open |
| [#325](https://github.com/stevearc/oil.nvim/issues/325) | oil-ssh error from command line | open |
| [#330](https://github.com/stevearc/oil.nvim/issues/330) | Telescope opens file in oil float | not actionable — cross-plugin, no repro |
| [#332](https://github.com/stevearc/oil.nvim/issues/332) | Buffer not fixed to floating window | open |
| [#335](https://github.com/stevearc/oil.nvim/issues/335) | Disable editing outside root dir | open |
| [#349](https://github.com/stevearc/oil.nvim/issues/349) | Parent directory as column/vsplit | open |
| [#351](https://github.com/stevearc/oil.nvim/issues/351) | Paste deleted file from register | open |
| [#359](https://github.com/stevearc/oil.nvim/issues/359) | Parse error on filenames differing by space | not actionable — parser uses whitespace as column delimiter |
| [#360](https://github.com/stevearc/oil.nvim/issues/360) | Pick window to open file into | open |
| [#362](https://github.com/stevearc/oil.nvim/issues/362) | "Could not find oil adapter for scheme" | not actionable — no repro, old nvim (0.9.5) |
| [#363](https://github.com/stevearc/oil.nvim/issues/363) | `prompt_save_on_select_new_entry` wrong prompt | fixed |
| [#371](https://github.com/stevearc/oil.nvim/issues/371) | Constrain cursor in insert mode | fixed ([#93](https://github.com/barrettruth/canola.nvim/pull/93)) |
| [#373](https://github.com/stevearc/oil.nvim/issues/373) | Dir from quickfix with bqf/trouble broken | open |
| [#375](https://github.com/stevearc/oil.nvim/issues/375) | Highlights for file types and permissions | open |
| [#380](https://github.com/stevearc/oil.nvim/issues/380) | Silently overriding `show_hidden` | not actionable — counter to config intent |
| [#382](https://github.com/stevearc/oil.nvim/issues/382) | Relative path in window title | open |
| [#392](https://github.com/stevearc/oil.nvim/issues/392) | Option to skip delete prompt | fixed |
| [#393](https://github.com/stevearc/oil.nvim/issues/393) | Auto-save on select | fixed |
| [#396](https://github.com/stevearc/oil.nvim/issues/396) | Customize preview content | open |
| [#399](https://github.com/stevearc/oil.nvim/issues/399) | Open file without closing Oil | open |
| [#404](https://github.com/stevearc/oil.nvim/issues/404) | Restricted UNC paths | not actionable — Windows-only |
| [#416](https://github.com/stevearc/oil.nvim/issues/416) | Cannot remap key to open split | open |
| [#431](https://github.com/stevearc/oil.nvim/issues/431) | More SSH adapter documentation | open |
| [#435](https://github.com/stevearc/oil.nvim/issues/435) | Error previewing with semantic tokens LSP | open |
| [#436](https://github.com/stevearc/oil.nvim/issues/436) | Owner and group columns | open |
| [#444](https://github.com/stevearc/oil.nvim/issues/444) | Opening behaviour customization | open |
| [#446](https://github.com/stevearc/oil.nvim/issues/446) | Executable highlighting | cherry-picked ([#698](https://github.com/stevearc/oil.nvim/pull/698)) |
| [#449](https://github.com/stevearc/oil.nvim/issues/449) | Renaming TypeScript files stopped working | open |
| [#450](https://github.com/stevearc/oil.nvim/issues/450) | Highlight opened file in directory listing | open |
| [#457](https://github.com/stevearc/oil.nvim/issues/457) | Custom column API | open |
| [#466](https://github.com/stevearc/oil.nvim/issues/466) | Select into window on right | open |
| [#473](https://github.com/stevearc/oil.nvim/issues/473) | Show hidden when dir is all-hidden | fixed ([#85](https://github.com/barrettruth/canola.nvim/pull/85)) |
| [#479](https://github.com/stevearc/oil.nvim/issues/479) | Harpoon integration recipe | open |
| [#483](https://github.com/stevearc/oil.nvim/issues/483) | Spell downloads depend on netrw | not actionable — fixed in neovim#34940 |
| [#486](https://github.com/stevearc/oil.nvim/issues/486) | Directory sizes show misleading 4.1k | fixed ([#87](https://github.com/barrettruth/canola.nvim/pull/87)) |
| [#492](https://github.com/stevearc/oil.nvim/issues/492) | j/k remapping question | not actionable — answered |
| [#507](https://github.com/stevearc/oil.nvim/issues/507) | lacasitos.nvim conflict | not actionable — cross-plugin + Windows-only |
| [#521](https://github.com/stevearc/oil.nvim/issues/521) | oil-ssh connection issues | open |
| [#525](https://github.com/stevearc/oil.nvim/issues/525) | SSH adapter documentation | open |
| [#531](https://github.com/stevearc/oil.nvim/issues/531) | Incomplete drive letters | not actionable — Windows-only |
| [#533](https://github.com/stevearc/oil.nvim/issues/533) | `constrain_cursor` bug | not actionable — needs repro |
| [#570](https://github.com/stevearc/oil.nvim/issues/570) | Improve c0/d0 for renaming | open |
| [#571](https://github.com/stevearc/oil.nvim/issues/571) | Callback before `highlight_filename` | open |
| [#578](https://github.com/stevearc/oil.nvim/issues/578) | Hidden file dimming recipe | fixed |
| [#587](https://github.com/stevearc/oil.nvim/issues/587) | Alt+h keymap | not actionable — user config issue |
| [#599](https://github.com/stevearc/oil.nvim/issues/599) | user:group display and manipulation | open |
| [#607](https://github.com/stevearc/oil.nvim/issues/607) | Per-host SCP args | open |
| [#609](https://github.com/stevearc/oil.nvim/issues/609) | Cursor placement via Snacks picker | not actionable — Windows-only |
| [#612](https://github.com/stevearc/oil.nvim/issues/612) | Delete buffers on file delete | fixed |
| [#615](https://github.com/stevearc/oil.nvim/issues/615) | Cursor at name column on o/O | fixed ([#72](https://github.com/barrettruth/canola.nvim/pull/72)) |
| [#617](https://github.com/stevearc/oil.nvim/issues/617) | Filetype by actual filetype | open |
| [#621](https://github.com/stevearc/oil.nvim/issues/621) | `toggle()` for regular windows | fixed ([#88](https://github.com/barrettruth/canola.nvim/pull/88)) |
| [#623](https://github.com/stevearc/oil.nvim/issues/623) | bufferline.nvim interaction | not actionable — cross-plugin |
| [#624](https://github.com/stevearc/oil.nvim/issues/624) | Mutation race | not actionable — no reliable repro |
| [#625](https://github.com/stevearc/oil.nvim/issues/625) | E19 mark invalid line | not actionable — intractable without neovim API changes |
| [#632](https://github.com/stevearc/oil.nvim/issues/632) | Preview + move = copy | fixed ([#12](https://github.com/barrettruth/canola.nvim/pull/12)) |
| [#636](https://github.com/stevearc/oil.nvim/issues/636) | Telescope picker opens in active buffer | not actionable — cannot reproduce |
| [#637](https://github.com/stevearc/oil.nvim/issues/637) | Inconsistent symlink resolution | not actionable — nightly-only, no stable repro |
| [#641](https://github.com/stevearc/oil.nvim/issues/641) | Flicker on `actions.parent` | open |
| [#642](https://github.com/stevearc/oil.nvim/issues/642) | W10 warning under `nvim -R` | fixed |
| [#645](https://github.com/stevearc/oil.nvim/issues/645) | `close_float` action | fixed |
| [#646](https://github.com/stevearc/oil.nvim/issues/646) | `get_current_dir` nil on SSH | open |
| [#650](https://github.com/stevearc/oil.nvim/issues/650) | LSP `workspace.fileOperations` events | fixed |
| [#655](https://github.com/stevearc/oil.nvim/issues/655) | File statistics as virtual text | open |
| [#659](https://github.com/stevearc/oil.nvim/issues/659) | Mark and diff files in buffer | open |
| [#664](https://github.com/stevearc/oil.nvim/issues/664) | Session reload extra buffer | not actionable — no repro |
| [#665](https://github.com/stevearc/oil.nvim/issues/665) | Hot load preview fast-scratch buffers | not actionable — no clear architecture |
| [#668](https://github.com/stevearc/oil.nvim/issues/668) | Custom yes/no confirmation | not actionable — no demand |
| [#670](https://github.com/stevearc/oil.nvim/issues/670) | Multi-directory cmdline args ignored | fixed ([#11](https://github.com/barrettruth/canola.nvim/pull/11)) |
| [#671](https://github.com/stevearc/oil.nvim/issues/671) | Yanking between nvim instances | not actionable — addressed upstream by clipboard actions |
| [#673](https://github.com/stevearc/oil.nvim/issues/673) | Symlink newlines crash | fixed |
| [#675](https://github.com/stevearc/oil.nvim/issues/675) | Move file into folder by renaming | open |
| [#676](https://github.com/stevearc/oil.nvim/issues/676) | Windows path conversion | not actionable — Windows-only |
| [#678](https://github.com/stevearc/oil.nvim/issues/678) | `buftype='acwrite'` causes `mksession` to skip oil windows | open |
| [#679](https://github.com/stevearc/oil.nvim/issues/679) | Executable file sign | cherry-picked ([#698](https://github.com/stevearc/oil.nvim/pull/698)) |
| [#682](https://github.com/stevearc/oil.nvim/issues/682) | `get_current_dir()` nil | cherry-picked ([#727](https://github.com/stevearc/oil.nvim/pull/727)) |
| [#683](https://github.com/stevearc/oil.nvim/issues/683) | Path not shown in floating mode | fixed |
| [#684](https://github.com/stevearc/oil.nvim/issues/684) | User and group columns | open |
| [#685](https://github.com/stevearc/oil.nvim/issues/685) | Plain directory paths in buffer names | not actionable — protocol prefix is fundamental to buffer identity |
| [#690](https://github.com/stevearc/oil.nvim/issues/690) | `OilFileIcon` highlight group | fixed |
| [#692](https://github.com/stevearc/oil.nvim/issues/692) | Keymap normalization | cherry-picked ([#725](https://github.com/stevearc/oil.nvim/pull/725)) |
| [#699](https://github.com/stevearc/oil.nvim/issues/699) | `select` blocks UI with slow FileType autocmd | fixed ([#106](https://github.com/barrettruth/canola.nvim/pull/106)) |
| [#707](https://github.com/stevearc/oil.nvim/issues/707) | Move file/dir into new dir by renaming | open |
| [#710](https://github.com/stevearc/oil.nvim/issues/710) | buftype empty on BufEnter | fixed ([#10](https://github.com/barrettruth/canola.nvim/pull/10)) |
| [#714](https://github.com/stevearc/oil.nvim/issues/714) | Support question | not actionable — answered |
| [#719](https://github.com/stevearc/oil.nvim/issues/719) | Neovim crash on node_modules | not actionable — libuv/neovim bug |
| [#726](https://github.com/stevearc/oil.nvim/issues/726) | Meta discussion/roadmap | not actionable |
| [#736](https://github.com/stevearc/oil.nvim/issues/736) | Make icons virtual text | open |
| [#738](https://github.com/stevearc/oil.nvim/issues/738) | Allow changing mtime/atime via time column | open |

43
flake.lock generated Normal file
View file

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

36
flake.nix Normal file
View file

@ -0,0 +1,36 @@
{
description = "canola.nvim refined fork of oil.nvim";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
systems.url = "github:nix-systems/default";
};
outputs =
{
nixpkgs,
systems,
...
}:
let
forEachSystem =
f: nixpkgs.lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system});
in
{
formatter = forEachSystem (pkgs: pkgs.nixfmt-tree);
devShells = forEachSystem (pkgs: {
default = pkgs.mkShell {
packages = [
pkgs.prettier
pkgs.stylua
pkgs.selene
(pkgs.luajit.withPackages (ps: [
ps.busted
ps.nlua
]))
];
};
});
};
}

View file

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

View file

@ -1,12 +1,12 @@
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 git = require("oil.git")
local log = require("oil.log")
local permissions = require("oil.adapters.files.permissions")
local util = require("oil.util")
local cache = require('canola.cache')
local columns = require('canola.columns')
local config = require('canola.config')
local constants = require('canola.constants')
local fs = require('canola.fs')
local git = require('canola.git')
local log = require('canola.log')
local permissions = require('canola.adapters.files.permissions')
local util = require('canola.util')
local uv = vim.uv or vim.loop
local M = {}
@ -25,7 +25,7 @@ local function read_link_data(path, cb)
assert(link)
local stat_path = link
if not fs.is_absolute(link) then
stat_path = fs.join(vim.fn.fnamemodify(path, ":h"), link)
stat_path = fs.join(vim.fn.fnamemodify(path, ':h'), link)
end
uv.fs_stat(stat_path, function(stat_err, stat)
cb(nil, link, stat)
@ -35,15 +35,15 @@ 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
---@class (exact) canola.FilesAdapter: canola.Adapter
---@field to_short_os_path fun(path: string, entry_type: nil|canola.EntryType): string
---@param path string
---@param entry_type nil|oil.EntryType
---@param entry_type nil|canola.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
if entry_type == 'directory' then
shortpath = util.addslash(shortpath, true)
end
return shortpath
@ -60,14 +60,17 @@ file_columns.size = {
if not stat then
return columns.EMPTY
end
if entry[FIELD_TYPE] == 'directory' then
return columns.EMPTY
end
if stat.size >= 1e9 then
return string.format("%.1fG", stat.size / 1e9)
return string.format('%.1fG', stat.size / 1e9)
elseif stat.size >= 1e6 then
return string.format("%.1fM", stat.size / 1e6)
return string.format('%.1fM', stat.size / 1e6)
elseif stat.size >= 1e3 then
return string.format("%.1fk", stat.size / 1e3)
return string.format('%.1fk', stat.size / 1e3)
else
return string.format("%d", stat.size)
return string.format('%d', stat.size)
end
end,
@ -82,7 +85,7 @@ file_columns.size = {
end,
parse = function(line, conf)
return line:match("^(%d+%S*)%s+(.*)$")
return line:match('^(%d+%S*)%s+(.*)$')
end,
}
@ -120,7 +123,7 @@ if not fs.is_windows then
local _, path = util.parse_url(action.url)
assert(path)
return string.format(
"CHMOD %s %s",
'CHMOD %s %s',
permissions.mode_to_octal_str(action.value),
M.to_short_os_path(path, action.entry_type)
)
@ -147,10 +150,10 @@ 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")
current_year = vim.fn.strftime('%Y')
end)
for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
for _, time_key in ipairs({ 'ctime', 'mtime', 'atime', 'birthtime' }) do
file_columns[time_key] = {
require_stat = true,
@ -165,11 +168,11 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
if fmt then
ret = vim.fn.strftime(fmt, stat[time_key].sec)
else
local year = vim.fn.strftime("%Y", stat[time_key].sec)
local year = vim.fn.strftime('%Y', stat[time_key].sec)
if year ~= current_year then
ret = vim.fn.strftime("%b %d %Y", stat[time_key].sec)
ret = vim.fn.strftime('%b %d %Y', stat[time_key].sec)
else
ret = vim.fn.strftime("%b %d %H:%M", stat[time_key].sec)
ret = vim.fn.strftime('%b %d %H:%M', stat[time_key].sec)
end
end
return ret
@ -183,20 +186,20 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
-- 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+")
:gsub('%%.', '%%S+')
:gsub('%s+', '%%s+')
-- escape `()[]` because those are special characters in Lua patterns
:gsub(
"%(",
"%%("
'%(',
'%%('
)
:gsub("%)", "%%)")
:gsub("%[", "%%[")
:gsub("%]", "%%]")
:gsub('%)', '%%)')
:gsub('%[', '%%[')
:gsub('%]', '%%]')
else
pattern = "%S+%s+%d+%s+%d%d:?%d%d"
pattern = '%S+%s+%d+%s+%d%d:?%d%d'
end
return line:match("^(" .. pattern .. ")%s+(.+)$")
return line:match('^(' .. pattern .. ')%s+(.+)$')
end,
get_sort_value = function(entry)
@ -226,7 +229,7 @@ local function columns_require_stat(column_defs)
end
---@param name string
---@return nil|oil.ColumnDefinition
---@return nil|canola.ColumnDefinition
M.get_column = function(name)
return file_columns[name]
end
@ -238,17 +241,17 @@ M.normalize_url = function(url, callback)
assert(path)
if fs.is_windows then
if path == "/" then
if path == '/' then
return callback(url)
else
local is_root_drive = path:match("^/%u$")
local is_root_drive = path:match('^/%u$')
if is_root_drive then
return callback(url .. "/")
return callback(url .. '/')
end
end
end
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p")
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ':p')
uv.fs_realpath(os_path, function(err, new_os_path)
local realpath
if fs.is_windows then
@ -264,8 +267,8 @@ M.normalize_url = function(url, callback)
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
is_directory = stat.type == 'directory'
elseif vim.endswith(realpath, '/') or (fs.is_windows and vim.endswith(realpath, '\\')) then
is_directory = true
else
local filetype = vim.filetype.match({ filename = vim.fs.basename(realpath) })
@ -276,7 +279,7 @@ 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(vim.fn.fnamemodify(realpath, ':.'))
end
end)
)
@ -284,7 +287,7 @@ M.normalize_url = function(url, callback)
end
---@param url string
---@param entry oil.Entry
---@param entry canola.Entry
---@param cb fun(path: nil|string)
M.get_entry_path = function(url, entry, cb)
if entry.id then
@ -292,18 +295,18 @@ M.get_entry_path = function(url, entry, cb)
local scheme, path = util.parse_url(parent_url)
M.normalize_url(scheme .. path .. entry.name, cb)
else
if entry.type == "directory" then
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")
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 entry canola.InternalEntry
---@param require_stat boolean
---@param cb fun(err?: string)
local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
@ -315,16 +318,16 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
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
-- See https://github.com/stevearc/canola.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
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)
log.warn('Error reading link data %s: %s', entry_path, link_err)
return cb()
end
meta.link = link
@ -336,7 +339,7 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
-- The link is broken, so let's use the stat of the link itself
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)
log.warn('Error lstat link file %s: %s', entry_path, stat_err)
return cb()
end
meta.stat = stat
@ -350,7 +353,7 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
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)
log.warn('Error stat file %s: %s', entry_path, stat_err)
return cb()
end
assert(stat)
@ -364,15 +367,15 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
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
-- See https://github.com/stevearc/canola.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
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)
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)
@ -393,22 +396,22 @@ end
---@param url string
---@param column_defs string[]
---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
---@param cb fun(err?: string, entries?: canola.InternalEntry[], fetch_more?: fun())
local function list_windows_drives(url, column_defs, cb)
local _, path = util.parse_url(url)
assert(path)
local require_stat = columns_require_stat(column_defs)
local stdout = ""
local jid = vim.fn.jobstart({ "wmic", "logicaldisk", "get", "name" }, {
local stdout = ''
local jid = vim.fn.jobstart({ 'wmic', 'logicaldisk', 'get', 'name' }, {
stdout_buffered = true,
on_stdout = function(_, data)
stdout = table.concat(data, "\n")
stdout = table.concat(data, '\n')
end,
on_exit = function(_, code)
if code ~= 0 then
return cb("Error listing windows devices")
return cb('Error listing windows devices')
end
local lines = vim.split(stdout, "\n", { plain = true, trimempty = true })
local lines = vim.split(stdout, '\n', { plain = true, trimempty = true })
-- Remove the "Name" header
table.remove(lines, 1)
local internal_entries = {}
@ -421,12 +424,12 @@ local function list_windows_drives(url, column_defs, cb)
end)
for _, disk in ipairs(lines) do
if disk:match("^%s*$") then
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")
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
@ -434,17 +437,17 @@ local function list_windows_drives(url, column_defs, cb)
end,
})
if jid <= 0 then
cb("Could not list windows devices")
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())
---@param cb fun(err?: string, entries?: canola.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
if fs.is_windows and path == '/' then
return list_windows_drives(url, column_defs, cb)
end
local dir = fs.posix_to_os_path(path)
@ -453,7 +456,7 @@ M.list = function(url, column_defs, cb)
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
uv.fs_opendir(dir, function(open_err, fd)
if open_err then
if open_err:match("^ENOENT: no such file or directory") then
if open_err:match('^ENOENT: no such file or directory') then
-- If the directory doesn't exist, treat the list as a success. We will be able to traverse
-- and edit a not-yet-existing directory.
return cb()
@ -505,7 +508,7 @@ 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
if fs.is_windows and path == '/' then
return false
end
local dir = fs.posix_to_os_path(path)
@ -515,30 +518,30 @@ M.is_modifiable = function(bufnr)
end
-- fs_access can return nil, force boolean return
return uv.fs_access(dir, "W") == true
return uv.fs_access(dir, 'W') == true
end
---@param action oil.Action
---@param action canola.Action
---@return string
M.render_action = function(action)
if action.type == "create" then
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))
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)
ret = ret .. ' -> ' .. fs.posix_to_os_path(action.link)
end
return ret
elseif action.type == "delete" then
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)
return string.format(' TRASH %s', short_path)
else
return string.format("DELETE %s", short_path)
return string.format('DELETE %s', short_path)
end
elseif action.type == "move" or action.type == "copy" then
elseif action.type == 'move' or action.type == 'copy' then
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if dest_adapter == M then
local _, src_path = util.parse_url(action.src_url)
@ -546,7 +549,7 @@ M.render_action = function(action)
local _, dest_path = util.parse_url(action.dest_url)
assert(dest_path)
return string.format(
" %s %s -> %s",
' %s %s -> %s',
action.type:upper(),
M.to_short_os_path(src_path, action.entry_type),
M.to_short_os_path(dest_path, action.entry_type)
@ -560,10 +563,10 @@ M.render_action = function(action)
end
end
---@param action oil.Action
---@param action canola.Action
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
if action.type == "create" then
if action.type == 'create' then
local _, path = util.parse_url(action.url)
assert(path)
path = fs.posix_to_os_path(path)
@ -579,16 +582,16 @@ M.perform_action = function(action, cb)
end)
end
if action.entry_type == "directory" then
if action.entry_type == 'directory' then
uv.fs_mkdir(path, config.new_dir_mode, function(err)
-- Ignore if the directory already exists
if not err or err:match("^EEXIST:") then
if not err or err:match('^EEXIST:') then
cb()
else
cb(err)
end
end)
elseif action.entry_type == "link" and action.link then
elseif action.entry_type == 'link' and action.link then
local flags = nil
local target = fs.posix_to_os_path(action.link)
if fs.is_windows then
@ -600,9 +603,21 @@ M.perform_action = function(action, cb)
---@diagnostic disable-next-line: param-type-mismatch
uv.fs_symlink(target, path, flags, cb)
else
fs.touch(path, config.new_file_mode, cb)
fs.touch(
path,
config.new_file_mode,
vim.schedule_wrap(function(err)
if not err then
vim.api.nvim_exec_autocmds(
'User',
{ pattern = 'CanolaFileCreated', modeline = false, data = { path = path } }
)
end
cb(err)
end)
)
end
elseif action.type == "delete" then
elseif action.type == 'delete' then
local _, path = util.parse_url(action.url)
assert(path)
path = fs.posix_to_os_path(path)
@ -619,11 +634,11 @@ M.perform_action = function(action, cb)
end
if config.delete_to_trash then
require("oil.adapters.trash").delete_to_trash(path, cb)
require('canola.adapters.trash').delete_to_trash(path, cb)
else
fs.recursive_delete(action.entry_type, path, cb)
end
elseif action.type == "move" then
elseif action.type == 'move' then
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if dest_adapter == M then
local _, src_path = util.parse_url(action.src_url)
@ -641,7 +656,7 @@ M.perform_action = function(action, cb)
-- We should never hit this because we don't implement supported_cross_adapter_actions
cb("files adapter doesn't support cross-adapter move")
end
elseif action.type == "copy" then
elseif action.type == 'copy' then
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if dest_adapter == M then
local _, src_path = util.parse_url(action.src_url)
@ -656,7 +671,7 @@ M.perform_action = function(action, cb)
cb("files adapter doesn't support cross-adapter copy")
end
else
cb(string.format("Bad action type: %s", action.type))
cb(string.format('Bad action type: %s', action.type))
end
end

View file

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

View file

@ -1,78 +1,79 @@
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 config = require('canola.config')
local constants = require('canola.constants')
local files = require('canola.adapters.files')
local fs = require('canola.fs')
local loading = require('canola.loading')
local pathutil = require('canola.pathutil')
local s3fs = require('canola.adapters.s3.s3fs')
local util = require('canola.util')
local M = {}
local FIELD_TYPE = constants.FIELD_TYPE
local FIELD_META = constants.FIELD_META
---@class (exact) oil.s3Url
---@class (exact) canola.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))
---@param canola_url string
---@return canola.s3Url
M.parse_url = function(canola_url)
local scheme, url = util.parse_url(canola_url)
assert(scheme and url, string.format("Malformed input url '%s'", canola_url))
local ret = { scheme = scheme }
local bucket, path = url:match("^([^/]+)/?(.*)$")
local bucket, path = url:match('^([^/]+)/?(.*)$')
ret.bucket = bucket
ret.path = path ~= "" and path or nil
ret.path = path ~= '' and path or nil
if not ret.bucket and ret.path then
error(string.format("Parsing error for s3 url: %s", oil_url))
error(string.format('Parsing error for s3 url: %s', canola_url))
end
---@cast ret oil.s3Url
---@cast ret canola.s3Url
return ret
end
---@param url oil.s3Url
---@param url canola.s3Url
---@return string
local function url_to_str(url)
local pieces = { url.scheme }
if url.bucket then
assert(url.bucket ~= "")
assert(url.bucket ~= '')
table.insert(pieces, url.bucket)
table.insert(pieces, "/")
table.insert(pieces, '/')
end
if url.path then
assert(url.path ~= "")
assert(url.path ~= '')
table.insert(pieces, url.path)
end
return table.concat(pieces, "")
return table.concat(pieces, '')
end
---@param url oil.s3Url
---@param url canola.s3Url
---@param is_folder boolean
---@return string
local function url_to_s3(url, is_folder)
local pieces = { "s3://" }
local pieces = { 's3://' }
if url.bucket then
assert(url.bucket ~= "")
assert(url.bucket ~= '')
table.insert(pieces, url.bucket)
table.insert(pieces, "/")
table.insert(pieces, '/')
end
if url.path then
assert(url.path ~= "")
assert(url.path ~= '')
table.insert(pieces, url.path)
if is_folder and not vim.endswith(url.path, "/") then
table.insert(pieces, "/")
if is_folder and not vim.endswith(url.path, '/') then
table.insert(pieces, '/')
end
end
return table.concat(pieces, "")
return table.concat(pieces, '')
end
---@param url oil.s3Url
---@param url canola.s3Url
---@return boolean
local function is_bucket(url)
assert(url.bucket and url.bucket ~= "")
assert(url.bucket and url.bucket ~= '')
if url.path then
assert(url.path ~= "")
assert(url.path ~= '')
return false
end
return true
@ -83,20 +84,24 @@ 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)
return ''
end
if entry[FIELD_TYPE] == 'directory' then
return ''
end
if meta.size >= 1e9 then
return string.format('%.1fG', meta.size / 1e9)
elseif meta.size >= 1e6 then
return string.format("%.1fM", meta.size / 1e6)
return string.format('%.1fM', meta.size / 1e6)
elseif meta.size >= 1e3 then
return string.format("%.1fk", meta.size / 1e3)
return string.format('%.1fk', meta.size / 1e3)
else
return string.format("%d", meta.size)
return string.format('%d', meta.size)
end
end,
parse = function(line, conf)
return line:match("^(%d+%S*)%s+(.*)$")
return line:match('^(%d+%S*)%s+(.*)$')
end,
get_sort_value = function(entry)
@ -113,21 +118,21 @@ s3_columns.birthtime = {
render = function(entry, conf)
local meta = entry[FIELD_META]
if not meta or not meta.date then
return ""
return ''
else
return meta.date
end
end,
parse = function(line, conf)
return line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$")
return line:match('^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$')
end,
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+)$")
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
@ -138,7 +143,7 @@ s3_columns.birthtime = {
}
---@param name string
---@return nil|oil.ColumnDefinition
---@return nil|canola.ColumnDefinition
M.get_column = function(name)
return s3_columns[name]
end
@ -148,9 +153,9 @@ end
M.get_parent = function(bufname)
local res = M.parse_url(bufname)
if res.path then
assert(res.path ~= "")
assert(res.path ~= '')
local path = pathutil.parent(res.path)
res.path = path ~= "" and path or nil
res.path = path ~= '' and path or nil
else
res.bucket = nil
end
@ -166,10 +171,10 @@ end
---@param url string
---@param column_defs string[]
---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
---@param callback fun(err?: string, entries?: canola.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`?")
if vim.fn.executable('aws') ~= 1 then
callback('`aws` is not executable. Can you run `aws s3 ls`?')
return
end
@ -184,19 +189,19 @@ M.is_modifiable = function(bufnr)
return true
end
---@param action oil.Action
---@param action canola.Action
---@return string
M.render_action = function(action)
local is_folder = action.entry_type == "directory"
if action.type == "create" then
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 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 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
@ -210,39 +215,39 @@ M.render_action = function(action)
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)
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 action canola.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 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
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
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))
cb(string.format('Bad entry type on s3 create action: %s', action.entry_type))
end
elseif action.type == "delete" then
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
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
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))
cb(string.format('Bad entry type on s3 delete action: %s', action.entry_type))
end
elseif action.type == "move" then
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
@ -250,7 +255,7 @@ M.perform_action = function(action, cb)
then
cb(
string.format(
"We should never attempt to move from the %s adapter to the %s adapter.",
'We should never attempt to move from the %s adapter to the %s adapter.',
src_adapter.name,
dest_adapter.name
)
@ -276,7 +281,7 @@ M.perform_action = function(action, cb)
assert(dest)
s3fs.mv(src, dest, is_folder, cb)
elseif action.type == "copy" then
elseif action.type == 'copy' then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if
@ -284,7 +289,7 @@ M.perform_action = function(action, cb)
then
cb(
string.format(
"We should never attempt to copy from the %s adapter to the %s adapter.",
'We should never attempt to copy from the %s adapter to the %s adapter.',
src_adapter.name,
dest_adapter.name
)
@ -311,11 +316,11 @@ M.perform_action = function(action, cb)
s3fs.cp(src, dest, is_folder, cb)
else
cb(string.format("Bad action type: %s", action.type))
cb(string.format('Bad action type: %s', action.type))
end
end
M.supported_cross_adapter_actions = { files = "move" }
M.supported_cross_adapter_actions = { files = 'move' }
---@param bufnr integer
M.read_file = function(bufnr)
@ -323,11 +328,11 @@ M.read_file = function(bufnr)
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")
local cache_dir = vim.fn.stdpath('cache')
assert(type(cache_dir) == 'string')
local tmpdir = fs.join(cache_dir, 'canola')
fs.mkdirp(tmpdir)
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "s3_XXXXXX"))
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, 's3_XXXXXX'))
if fd then
vim.loop.fs_close(fd)
end
@ -336,9 +341,9 @@ M.read_file = function(bufnr)
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 } })
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"))
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()
@ -352,7 +357,7 @@ M.read_file = function(bufnr)
if filetype then
vim.bo[bufnr].filetype = filetype
end
vim.cmd.doautocmd({ args = { "BufReadPost", bufname }, mods = { silent = true } })
vim.cmd.doautocmd({ args = { 'BufReadPost', bufname }, mods = { silent = true } })
vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
end)
end
@ -361,14 +366,14 @@ end
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"))
local cache_dir = vim.fn.stdpath('cache')
assert(type(cache_dir) == 'string')
local tmpdir = fs.join(cache_dir, 'canola')
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.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)
@ -376,10 +381,10 @@ M.write_file = function(bufnr)
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)
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 } })
vim.cmd.doautocmd({ args = { 'BufWritePost', bufname }, mods = { silent = true } })
end
vim.loop.fs_unlink(tmpfile)
vim.api.nvim_buf_delete(tmp_bufnr, { force = true })

View file

@ -1,8 +1,8 @@
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 cache = require('canola.cache')
local config = require('canola.config')
local constants = require('canola.constants')
local shell = require('canola.shell')
local util = require('canola.util')
local M = {}
@ -10,35 +10,35 @@ local FIELD_META = constants.FIELD_META
---@param line string
---@return string Name of entry
---@return oil.EntryType
---@return canola.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+(.*)$")
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 type = 'directory'
local meta = { date = date }
return name, type, meta
end
---@param line string
---@return string Name of entry
---@return oil.EntryType
---@return canola.EntryType
---@return table Metadata for entry
local function parse_ls_line_file(line)
local name = line:match("^%s+PRE%s+(.*)/$")
local type = "directory"
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+(.*)$")
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"
type = 'file'
meta = { date = date, size = tonumber(size) }
return name, type, meta
end
@ -46,15 +46,15 @@ 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)
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())
---@param callback fun(err?: string, entries?: canola.InternalEntry[], fetch_more?: fun())
function M.list_dir(url, path, callback)
local cmd = create_s3_command({ "ls", path, "--color=off", "--no-cli-pager" })
local cmd = create_s3_command({ 'ls', path, '--color=off', '--no-cli-pager' })
shell.run(cmd, function(err, lines)
if err then
return callback(err)
@ -63,13 +63,13 @@ function M.list_dir(url, path, callback)
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 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
if line ~= '' then
local name, type, meta = parse_ls_line(line)
-- in s3 '-' can be used to create an "empty folder"
if name ~= "-" then
if name ~= '-' then
local cache_entry = cache.create_entry(url, name, type)
table.insert(cache_entries, cache_entry)
cache_entry[FIELD_META] = meta
@ -85,8 +85,8 @@ end
---@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)
local cmd = create_s3_command({ 'cp', '-', path })
shell.run(cmd, { stdin = 'null' }, callback)
end
--- Remove files
@ -94,9 +94,9 @@ end
---@param is_folder boolean
---@param callback fun(err: nil|string)
function M.rm(path, is_folder, callback)
local main_cmd = { "rm", path }
local main_cmd = { 'rm', path }
if is_folder then
table.insert(main_cmd, "--recursive")
table.insert(main_cmd, '--recursive')
end
local cmd = create_s3_command(main_cmd)
shell.run(cmd, callback)
@ -106,7 +106,7 @@ end
---@param bucket string
---@param callback fun(err: nil|string)
function M.rb(bucket, callback)
local cmd = create_s3_command({ "rb", bucket })
local cmd = create_s3_command({ 'rb', bucket })
shell.run(cmd, callback)
end
@ -114,7 +114,7 @@ end
---@param bucket string
---@param callback fun(err: nil|string)
function M.mb(bucket, callback)
local cmd = create_s3_command({ "mb", bucket })
local cmd = create_s3_command({ 'mb', bucket })
shell.run(cmd, callback)
end
@ -124,9 +124,9 @@ end
---@param is_folder boolean
---@param callback fun(err: nil|string)
function M.mv(src, dest, is_folder, callback)
local main_cmd = { "mv", src, dest }
local main_cmd = { 'mv', src, dest }
if is_folder then
table.insert(main_cmd, "--recursive")
table.insert(main_cmd, '--recursive')
end
local cmd = create_s3_command(main_cmd)
shell.run(cmd, callback)
@ -138,9 +138,9 @@ end
---@param is_folder boolean
---@param callback fun(err: nil|string)
function M.cp(src, dest, is_folder, callback)
local main_cmd = { "cp", src, dest }
local main_cmd = { 'cp', src, dest }
if is_folder then
table.insert(main_cmd, "--recursive")
table.insert(main_cmd, '--recursive')
end
local cmd = create_s3_command(main_cmd)
shell.run(cmd, callback)

View file

@ -1,19 +1,20 @@
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 permissions = require("oil.adapters.files.permissions")
local shell = require("oil.shell")
local sshfs = require("oil.adapters.ssh.sshfs")
local util = require("oil.util")
local config = require('canola.config')
local constants = require('canola.constants')
local files = require('canola.adapters.files')
local fs = require('canola.fs')
local loading = require('canola.loading')
local pathutil = require('canola.pathutil')
local permissions = require('canola.adapters.files.permissions')
local shell = require('canola.shell')
local sshfs = require('canola.adapters.ssh.sshfs')
local util = require('canola.util')
local M = {}
local FIELD_NAME = constants.FIELD_NAME
local FIELD_TYPE = constants.FIELD_TYPE
local FIELD_META = constants.FIELD_META
---@class (exact) oil.sshUrl
---@class (exact) canola.sshUrl
---@field scheme string
---@field host string
---@field user nil|string
@ -22,75 +23,75 @@ local FIELD_META = constants.FIELD_META
---@param args string[]
local function scp(args, ...)
local cmd = vim.list_extend({ "scp", "-C" }, config.extra_scp_args)
local cmd = vim.list_extend({ 'scp', '-C' }, config.extra_scp_args)
vim.list_extend(cmd, args)
shell.run(cmd, ...)
end
---@param oil_url string
---@return oil.sshUrl
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))
---@param canola_url string
---@return canola.sshUrl
M.parse_url = function(canola_url)
local scheme, url = util.parse_url(canola_url)
assert(scheme and url, string.format("Malformed input url '%s'", canola_url))
local ret = { scheme = scheme }
local username, rem = url:match("^([^@%s]+)@(.*)$")
local username, rem = url:match('^([^@%s]+)@(.*)$')
ret.user = username
url = rem or url
local host, port, path = url:match("^([^:]+):(%d+)/(.*)$")
local host, port, path = url:match('^([^:]+):(%d+)/(.*)$')
if host then
ret.host = host
ret.port = tonumber(port)
ret.path = path
else
host, path = url:match("^([^/]+)/(.*)$")
host, path = url:match('^([^/]+)/(.*)$')
ret.host = host
ret.path = path
end
if not ret.host or not ret.path then
error(string.format("Malformed SSH url: %s", oil_url))
error(string.format('Malformed SSH url: %s', canola_url))
end
---@cast ret oil.sshUrl
---@cast ret canola.sshUrl
return ret
end
---@param url oil.sshUrl
---@param url canola.sshUrl
---@return string
local function url_to_str(url)
local pieces = { url.scheme }
if url.user then
table.insert(pieces, url.user)
table.insert(pieces, "@")
table.insert(pieces, '@')
end
table.insert(pieces, url.host)
if url.port then
table.insert(pieces, string.format(":%d", url.port))
table.insert(pieces, string.format(':%d', url.port))
end
table.insert(pieces, "/")
table.insert(pieces, '/')
table.insert(pieces, url.path)
return table.concat(pieces, "")
return table.concat(pieces, '')
end
---@param url oil.sshUrl
---@param url canola.sshUrl
---@return string
local function url_to_scp(url)
local pieces = { "scp://" }
local pieces = { 'scp://' }
if url.user then
table.insert(pieces, url.user)
table.insert(pieces, "@")
table.insert(pieces, '@')
end
table.insert(pieces, url.host)
if url.port then
table.insert(pieces, string.format(":%d", url.port))
table.insert(pieces, string.format(':%d', url.port))
end
table.insert(pieces, "/")
table.insert(pieces, '/')
local escaped_path = util.url_escape(url.path)
table.insert(pieces, escaped_path)
return table.concat(pieces, "")
return table.concat(pieces, '')
end
---@param url1 oil.sshUrl
---@param url2 oil.sshUrl
---@param url1 canola.sshUrl
---@param url2 canola.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
@ -99,11 +100,11 @@ end
local _connections = {}
---@param url string
---@param allow_retry nil|boolean
---@return oil.sshFs
---@return canola.sshFs
local function get_connection(url, allow_retry)
local res = M.parse_url(url)
res.scheme = config.adapter_to_scheme.ssh
res.path = ""
res.path = ''
local key = url_to_str(res)
local conn = _connections[key]
if not conn or (allow_retry and conn:get_connection_error()) then
@ -137,7 +138,7 @@ ssh_columns.permissions = {
end,
render_action = function(action)
return string.format("CHMOD %s %s", permissions.mode_to_octal_str(action.value), action.url)
return string.format('CHMOD %s %s', permissions.mode_to_octal_str(action.value), action.url)
end,
perform_action = function(action, callback)
@ -151,20 +152,24 @@ ssh_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)
return ''
end
if entry[FIELD_TYPE] == 'directory' then
return ''
end
if meta.size >= 1e9 then
return string.format('%.1fG', meta.size / 1e9)
elseif meta.size >= 1e6 then
return string.format("%.1fM", meta.size / 1e6)
return string.format('%.1fM', meta.size / 1e6)
elseif meta.size >= 1e3 then
return string.format("%.1fk", meta.size / 1e3)
return string.format('%.1fk', meta.size / 1e3)
else
return string.format("%d", meta.size)
return string.format('%d', meta.size)
end
end,
parse = function(line, conf)
return line:match("^(%d+%S*)%s+(.*)$")
return line:match('^(%d+%S*)%s+(.*)$')
end,
get_sort_value = function(entry)
@ -178,7 +183,7 @@ ssh_columns.size = {
}
---@param name string
---@return nil|oil.ColumnDefinition
---@return nil|canola.ColumnDefinition
M.get_column = function(name)
return ssh_columns[name]
end
@ -206,13 +211,13 @@ M.normalize_url = function(url, callback)
local conn = get_connection(url, true)
local path = res.path
if path == "" then
path = "."
if path == '' then
path = '.'
end
conn:realpath(path, function(err, abspath)
if err then
vim.notify(string.format("Error normalizing url %s: %s", url, err), vim.log.levels.WARN)
vim.notify(string.format('Error normalizing url %s: %s', url, err), vim.log.levels.WARN)
callback(url)
else
res.path = abspath
@ -223,7 +228,7 @@ end
---@param url string
---@param column_defs string[]
---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
---@param callback fun(err?: string, entries?: canola.InternalEntry[], fetch_more?: fun())
M.list = function(url, column_defs, callback)
local res = M.parse_url(url)
@ -256,18 +261,18 @@ M.is_modifiable = function(bufnr)
return bit.band(rwx, 2) ~= 0
end
---@param action oil.Action
---@param action canola.Action
---@return string
M.render_action = function(action)
if action.type == "create" then
local ret = string.format("CREATE %s", action.url)
if action.type == 'create' then
local ret = string.format('CREATE %s', action.url)
if action.link then
ret = ret .. " -> " .. action.link
ret = ret .. ' -> ' .. action.link
end
return ret
elseif action.type == "delete" then
return string.format("DELETE %s", action.url)
elseif action.type == "move" or action.type == "copy" then
elseif action.type == 'delete' then
return string.format('DELETE %s', action.url)
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
@ -279,30 +284,30 @@ M.render_action = function(action)
assert(path)
src = files.to_short_os_path(path, action.entry_type)
end
return string.format(" %s %s -> %s", action.type:upper(), src, dest)
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 action canola.Action
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
if action.type == "create" then
if action.type == 'create' then
local res = M.parse_url(action.url)
local conn = get_connection(action.url)
if action.entry_type == "directory" then
if action.entry_type == 'directory' then
conn:mkdir(res.path, cb)
elseif action.entry_type == "link" and action.link then
elseif action.entry_type == 'link' and action.link then
conn:mklink(res.path, action.link, cb)
else
conn:touch(res.path, cb)
end
elseif action.type == "delete" then
elseif action.type == 'delete' then
local res = M.parse_url(action.url)
local conn = get_connection(action.url)
conn:rm(res.path, cb)
elseif action.type == "move" then
elseif action.type == 'move' then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter == M and dest_adapter == M then
@ -311,7 +316,7 @@ M.perform_action = function(action, cb)
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)
scp({ '-r', url_to_scp(src_res), url_to_scp(dest_res) }, function(err)
if err then
return cb(err)
end
@ -321,16 +326,16 @@ M.perform_action = function(action, cb)
src_conn:mv(src_res.path, dest_res.path, cb)
end
else
cb("We should never attempt to move across adapters")
cb('We should never attempt to move across adapters')
end
elseif action.type == "copy" then
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 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)
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)
@ -349,14 +354,14 @@ M.perform_action = function(action, cb)
src_arg = fs.posix_to_os_path(path)
dest_arg = url_to_scp(M.parse_url(action.dest_url))
end
scp({ "-r", src_arg, dest_arg }, cb)
scp({ '-r', src_arg, dest_arg }, cb)
end
else
cb(string.format("Bad action type: %s", action.type))
cb(string.format('Bad action type: %s', action.type))
end
end
M.supported_cross_adapter_actions = { files = "copy" }
M.supported_cross_adapter_actions = { files = 'copy' }
---@param bufnr integer
M.read_file = function(bufnr)
@ -365,11 +370,11 @@ M.read_file = function(bufnr)
local url = M.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 cache_dir = vim.fn.stdpath('cache')
assert(type(cache_dir) == 'string')
local tmpdir = fs.join(cache_dir, 'canola')
fs.mkdirp(tmpdir)
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXX"))
local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, 'ssh_XXXXXX'))
if fd then
vim.loop.fs_close(fd)
end
@ -378,9 +383,9 @@ M.read_file = function(bufnr)
scp({ scp_url, tmpfile }, function(err)
loading.set_loading(bufnr, false)
vim.bo[bufnr].modifiable = true
vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { silent = true } })
vim.cmd.doautocmd({ args = { 'BufReadPre', bufname }, mods = { silent = true } })
if err then
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, vim.split(err, "\n"))
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, vim.split(err, '\n'))
else
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {})
vim.api.nvim_buf_call(bufnr, function()
@ -394,9 +399,9 @@ M.read_file = function(bufnr)
if filetype then
vim.bo[bufnr].filetype = filetype
end
vim.cmd.doautocmd({ args = { "BufReadPost", bufname }, mods = { silent = true } })
vim.cmd.doautocmd({ args = { 'BufReadPost', bufname }, mods = { silent = true } })
vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
vim.keymap.set("n", "gf", M.goto_file, { buffer = bufnr })
vim.keymap.set('n', 'gf', M.goto_file, { buffer = bufnr })
end)
end
@ -405,14 +410,14 @@ 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"))
local cache_dir = vim.fn.stdpath('cache')
assert(type(cache_dir) == 'string')
local tmpdir = fs.join(cache_dir, 'canola')
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.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)
@ -420,10 +425,10 @@ M.write_file = function(bufnr)
scp({ tmpfile, scp_url }, function(err)
vim.bo[bufnr].modifiable = true
if err then
vim.notify(string.format("Error writing file: %s", err), vim.log.levels.ERROR)
vim.notify(string.format('Error writing file: %s', err), vim.log.levels.ERROR)
else
vim.bo[bufnr].modified = false
vim.cmd.doautocmd({ args = { "BufWritePost", bufname }, mods = { silent = true } })
vim.cmd.doautocmd({ args = { 'BufWritePost', bufname }, mods = { silent = true } })
end
vim.loop.fs_unlink(tmpfile)
vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
@ -432,7 +437,7 @@ end
M.goto_file = function()
local url = M.parse_url(vim.api.nvim_buf_get_name(0))
local fname = vim.fn.expand("<cfile>")
local fname = vim.fn.expand('<cfile>')
local fullpath = fname
if not fs.is_absolute(fname) then
local pardir = vim.fs.dirname(url.path)
@ -441,7 +446,7 @@ M.goto_file = function()
url.path = vim.fs.dirname(fullpath)
local parurl = url_to_str(url)
---@cast M oil.Adapter
---@cast M canola.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)
@ -459,7 +464,7 @@ M.goto_file = function()
vim.cmd.edit({ args = { url_to_str(url) } })
return
end
for suffix in vim.gsplit(vim.o.suffixesadd, ",", { plain = true, trimempty = true }) do
for suffix in vim.gsplit(vim.o.suffixesadd, ',', { plain = true, trimempty = true }) do
local suffixname = basename .. suffix
if name_map[suffixname] then
url.path = fullpath .. suffix

View file

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

View file

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

View file

@ -1,5 +1,5 @@
local cache = require("oil.cache")
local util = require("oil.util")
local cache = require('canola.cache')
local util = require('canola.util')
local M = {}
---@param url string
@ -12,7 +12,7 @@ local dir_listing = {}
---@param url string
---@param column_defs string[]
---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
---@param cb fun(err?: string, entries?: canola.InternalEntry[], fetch_more?: fun())
M.list = function(url, column_defs, cb)
local _, path = util.parse_url(url)
local entries = dir_listing[path] or {}
@ -29,32 +29,32 @@ M.test_clear = function()
end
---@param path string
---@param entry_type oil.EntryType
---@return oil.InternalEntry
---@param entry_type canola.EntryType
---@return canola.InternalEntry
M.test_set = function(path, entry_type)
if path == "/" then
if path == '/' then
return {}
end
local parent = vim.fn.fnamemodify(path, ":h")
local parent = vim.fn.fnamemodify(path, ':h')
if parent ~= path then
M.test_set(parent, "directory")
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 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
local parent_url = 'canola-test://' .. parent
return cache.create_and_store_entry(parent_url, entry.name, entry.entry_type)
end
---@param name string
---@return nil|oil.ColumnDefinition
---@return nil|canola.ColumnDefinition
M.get_column = function(name)
return nil
end
@ -65,19 +65,19 @@ M.is_modifiable = function(bufnr)
return true
end
---@param action oil.Action
---@param action canola.Action
---@return string
M.render_action = function(action)
if action.type == "create" or action.type == "delete" then
return string.format("%s %s", action.type:upper(), action.url)
elseif action.type == "move" or action.type == "copy" then
return string.format(" %s %s -> %s", action.type:upper(), action.src_url, action.dest_url)
if action.type == 'create' or action.type == 'delete' then
return string.format('%s %s', action.type:upper(), action.url)
elseif action.type == 'move' or action.type == 'copy' then
return string.format(' %s %s -> %s', action.type:upper(), action.src_url, action.dest_url)
else
error("Bad action type")
error('Bad action type')
end
end
---@param action oil.Action
---@param action canola.Action
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
cb()

View file

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

View file

@ -1,11 +1,11 @@
-- Based on the FreeDesktop.org trash specification
-- https://specifications.freedesktop.org/trash/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 cache = require('canola.cache')
local config = require('canola.config')
local constants = require('canola.constants')
local files = require('canola.adapters.files')
local fs = require('canola.fs')
local util = require('canola.util')
local uv = vim.uv or vim.loop
local FIELD_META = constants.FIELD_META
@ -14,8 +14,8 @@ 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)
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
@ -23,9 +23,9 @@ end
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")
xdg_home = fs.join(assert(uv.os_homedir()), '.local', 'share')
end
local trash_dir = fs.join(xdg_home, "Trash")
local trash_dir = fs.join(xdg_home, 'Trash')
ensure_trash_dir(trash_dir)
return trash_dir
end
@ -43,13 +43,13 @@ end
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 })
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
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)
@ -58,7 +58,7 @@ local function get_top_trash_dirs(path)
-- Also search for the .Trash-$uid
top_trash_dirs = vim.fs.find(
string.format(".Trash-%d", uv.getuid()),
string.format('.Trash-%d', uv.getuid()),
{ upward = true, path = path, limit = math.huge }
)
for _, top_trash_dir in ipairs(top_trash_dirs) do
@ -91,14 +91,14 @@ local function get_write_trash_dir(path)
return top_trash_dirs[1]
end
local parent = vim.fn.fnamemodify(path, ":h")
local next_parent = vim.fn.fnamemodify(parent, ":h")
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")
next_parent = vim.fn.fnamemodify(parent, ':h')
end
local top_trash = fs.join(parent, string.format(".Trash-%d", uv.getuid()))
local top_trash = fs.join(parent, string.format('.Trash-%d', uv.getuid()))
ensure_trash_dir(top_trash)
return top_trash
end
@ -116,7 +116,7 @@ end
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")
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)
@ -127,12 +127,12 @@ M.normalize_url = function(url, callback)
end
---@param url string
---@param entry oil.Entry
---@param entry canola.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
---@type canola.TrashInfo
local trash_info = meta.trash_info
if not trash_info then
-- This is a subpath in the trash
@ -140,13 +140,13 @@ M.get_entry_path = function(url, entry, cb)
return
end
local path = fs.os_to_posix_path(trash_info.trash_file)
if meta.stat.type == "directory" then
if meta.stat.type == 'directory' then
path = util.addslash(path)
end
cb("oil://" .. path)
cb('canola://' .. path)
end
---@class oil.TrashInfo
---@class canola.TrashInfo
---@field trash_file string
---@field info_file string
---@field original_path string
@ -154,12 +154,12 @@ end
---@field stat uv.aliases.fs_stat_table
---@param info_file string
---@param cb fun(err?: string, info?: oil.TrashInfo)
---@param cb fun(err?: string, info?: canola.TrashInfo)
local function read_trash_info(info_file, cb)
if not vim.endswith(info_file, ".trashinfo") then
return cb("File is not .trashinfo")
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)
uv.fs_open(info_file, 'r', 448, function(err, fd)
if err then
return cb(err)
end
@ -182,35 +182,35 @@ local function read_trash_info(info_file, cb)
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")
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")
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
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)
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")
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)
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")
cb('.trashinfo file points to non-existant file')
else
trash_info.stat = trash_stat
---@cast trash_info oil.TrashInfo
---@cast trash_info canola.TrashInfo
cb(nil, trash_info)
end
end)
@ -222,7 +222,7 @@ end
---@param url string
---@param column_defs string[]
---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
---@param cb fun(err?: string, entries?: canola.InternalEntry[], fetch_more?: fun())
M.list = function(url, column_defs, cb)
cb = vim.schedule_wrap(cb)
local _, path = util.parse_url(url)
@ -244,14 +244,14 @@ M.list = function(url, column_defs, cb)
-- The first trash dir is a special case; it is in the home directory and we should only show
-- all entries if we are in the top root path "/"
if trash_idx == 1 then
show_all_files = path == "/"
show_all_files = path == '/'
end
local info_dir = fs.join(trash_dir, "info")
local info_dir = fs.join(trash_dir, 'info')
---@diagnostic disable-next-line: param-type-mismatch, discard-returns
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 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()
@ -286,12 +286,12 @@ M.list = function(url, column_defs, cb)
-- files.
poll()
else
local parent = util.addslash(vim.fn.fnamemodify(info.original_path, ":h"))
local parent = util.addslash(vim.fn.fnamemodify(info.original_path, ':h'))
if path == parent or show_all_files then
local name = vim.fn.fnamemodify(info.trash_file, ":t")
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")
local display_name = vim.fn.fnamemodify(info.original_path, ':t')
cache_entry[FIELD_META] = {
stat = info.stat,
trash_info = info,
@ -302,12 +302,12 @@ M.list = function(url, column_defs, cb)
if path ~= parent and (show_all_files or fs.is_subpath(path, parent)) then
local name = parent:sub(path:len() + 1)
local next_par = vim.fs.dirname(name)
while next_par ~= "." do
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")
local cache_entry = cache.create_entry(url, name, 'directory')
cache_entry[FIELD_META] = {
stat = info.stat,
@ -348,7 +348,7 @@ 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")
current_year = vim.fn.strftime('%Y')
end)
file_columns.mtime = {
@ -357,7 +357,7 @@ file_columns.mtime = {
if not meta then
return nil
end
---@type oil.TrashInfo
---@type canola.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
@ -368,11 +368,11 @@ file_columns.mtime = {
if fmt then
ret = vim.fn.strftime(fmt, time)
else
local year = vim.fn.strftime("%Y", time)
local year = vim.fn.strftime('%Y', time)
if year ~= current_year then
ret = vim.fn.strftime("%b %d %Y", time)
ret = vim.fn.strftime('%b %d %Y', time)
else
ret = vim.fn.strftime("%b %d %H:%M", time)
ret = vim.fn.strftime('%b %d %H:%M', time)
end
end
return ret
@ -380,7 +380,7 @@ file_columns.mtime = {
get_sort_value = function(entry)
local meta = entry[FIELD_META]
---@type nil|oil.TrashInfo
---@type nil|canola.TrashInfo
local trash_info = meta and meta.trash_info
if trash_info then
return trash_info.deletion_date
@ -393,104 +393,105 @@ file_columns.mtime = {
local fmt = conf and conf.format
local pattern
if fmt then
pattern = fmt:gsub("%%.", "%%S+")
pattern = fmt:gsub('%%.', '%%S+')
else
pattern = "%S+%s+%d+%s+%d%d:?%d%d"
pattern = '%S+%s+%d+%s+%d%d:?%d%d'
end
return line:match("^(" .. pattern .. ")%s+(.+)$")
return line:match('^(' .. pattern .. ')%s+(.+)$')
end,
}
---@param name string
---@return nil|oil.ColumnDefinition
---@return nil|canola.ColumnDefinition
M.get_column = function(name)
return file_columns[name]
end
M.supported_cross_adapter_actions = { files = "move" }
M.supported_cross_adapter_actions = { files = 'move' }
---@param action oil.Action
---@param action canola.Action
---@return boolean
M.filter_action = function(action)
if action.type == "create" then
if action.type == 'create' then
return false
elseif action.type == "delete" then
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
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
return src_adapter.name == 'files' or dest_adapter.name == 'files'
-- selene: allow(if_same_then_else)
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"
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
---@param err canola.ParseError
---@return boolean
M.filter_error = function(err)
if err.message == "Duplicate filename" then
if err.message == 'Duplicate filename' then
return false
end
return true
end
---@param action oil.Action
---@param action canola.Action
---@return string
M.render_action = function(action)
if action.type == "delete" then
if action.type == 'delete' then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
---@type oil.TrashInfo
---@type canola.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
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
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
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)
return string.format('RESTORE %s', short_path)
else
error("Must be moving files into or out of trash")
error('Must be moving files into or out of trash')
end
elseif action.type == "copy" then
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
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
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)
return string.format('RESTORE %s', short_path)
else
error("Must be copying files into or out of trash")
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 trash_info canola.TrashInfo
---@param cb fun(err?: string)
local function purge(trash_info, cb)
fs.recursive_delete("file", trash_info.info_file, function(err)
fs.recursive_delete('file', trash_info.info_file, function(err)
if err then
return cb(err)
end
@ -505,15 +506,15 @@ end
local function write_info_file(path, info_path, cb)
uv.fs_open(
info_path,
"w",
'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)
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)
@ -524,14 +525,14 @@ local function write_info_file(path, info_path, cb)
end
---@param path string
---@param cb fun(err?: string, trash_info?: oil.TrashInfo)
---@param cb fun(err?: string, trash_info?: canola.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")
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)
@ -541,7 +542,7 @@ local function create_trash_info(path, cb)
if info_err then
return cb(info_err)
end
---@type oil.TrashInfo
---@type canola.TrashInfo
local trash_info = {
original_path = path,
trash_file = dest_path,
@ -554,28 +555,28 @@ local function create_trash_info(path, cb)
end)
end
---@param action oil.Action
---@param action canola.Action
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
if action.type == "delete" then
if action.type == 'delete' then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
---@type oil.TrashInfo
---@type canola.TrashInfo
local trash_info = assert(meta).trash_info
purge(trash_info, cb)
elseif action.type == "move" then
elseif action.type == 'move' then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter.name == "files" then
if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url)
M.delete_to_trash(assert(path), cb)
elseif dest_adapter.name == "files" then
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
---@type canola.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
@ -584,36 +585,36 @@ M.perform_action = function(action, cb)
uv.fs_unlink(trash_info.info_file, cb)
end)
else
error("Must be moving files into or out of trash")
error('Must be moving files into or out of trash')
end
elseif action.type == "copy" then
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
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"
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
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
---@type canola.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")
error('Must be moving files into or out of trash')
end
else
cb(string.format("Bad action type: %s", action.type))
cb(string.format('Bad action type: %s', action.type))
end
end
@ -624,7 +625,7 @@ M.delete_to_trash = function(path, cb)
if err then
cb(err)
else
local stat_type = trash_info.stat.type or "unknown"
local stat_type = trash_info.stat.type or 'unknown'
fs.recursive_move(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb))
end
end)

View file

@ -1,8 +1,8 @@
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 cache = require('canola.cache')
local config = require('canola.config')
local files = require('canola.adapters.files')
local fs = require('canola.fs')
local util = require('canola.util')
local uv = vim.uv or vim.loop
@ -15,7 +15,7 @@ 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")
local trash_dir = fs.join(assert(uv.os_homedir()), '.Trash')
touch_dir(trash_dir)
return trash_dir
end
@ -25,24 +25,24 @@ end
M.normalize_url = function(url, callback)
local scheme, path = util.parse_url(url)
assert(path)
callback(scheme .. "/")
callback(scheme .. '/')
end
---@param url string
---@param entry oil.Entry
---@param entry canola.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
if entry.type == 'directory' then
path = 'canola://' .. path
end
cb(path)
end
---@param url string
---@param column_defs string[]
---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
---@param cb fun(err?: string, entries?: canola.InternalEntry[], fetch_more?: fun())
M.list = function(url, column_defs, cb)
cb = vim.schedule_wrap(cb)
local _, path = util.parse_url(url)
@ -51,7 +51,7 @@ M.list = function(url, column_defs, cb)
---@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 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()
@ -106,61 +106,61 @@ M.is_modifiable = function(bufnr)
end
---@param name string
---@return nil|oil.ColumnDefinition
---@return nil|canola.ColumnDefinition
M.get_column = function(name)
return nil
end
M.supported_cross_adapter_actions = { files = "move" }
M.supported_cross_adapter_actions = { files = 'move' }
---@param action oil.Action
---@param action canola.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
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
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
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)
return string.format('RESTORE %s', short_path)
else
return string.format(" %s %s -> %s", action.type:upper(), action.src_url, action.dest_url)
return string.format(' %s %s -> %s', action.type:upper(), action.src_url, action.dest_url)
end
elseif action.type == "copy" then
return string.format(" %s %s -> %s", action.type:upper(), action.src_url, action.dest_url)
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")
error('Bad action type')
end
end
---@param action oil.Action
---@param action canola.Action
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
local trash_dir = get_trash_dir()
if action.type == "create" then
if action.type == 'create' then
local _, path = util.parse_url(action.url)
assert(path)
path = trash_dir .. path
if action.entry_type == "directory" then
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
if not err or err:match('^EEXIST:') then
cb()
else
cb(err)
end
end) -- 0755
elseif action.entry_type == "link" and action.link then
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
@ -168,33 +168,33 @@ M.perform_action = function(action, cb)
else
fs.touch(path, config.new_file_mode, cb)
end
elseif action.type == "delete" then
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
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
if src_adapter.name == 'files' then
dest_path = trash_dir .. dest_path
elseif dest_adapter.name == "files" then
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
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))
cb(string.format('Bad action type: %s', action.type))
end
end
@ -212,8 +212,8 @@ M.delete_to_trash = function(path, cb)
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 })
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)

View file

@ -1,11 +1,11 @@
local util = require("oil.util")
local util = require('canola.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 cache = require('canola.cache')
local config = require('canola.config')
local constants = require('canola.constants')
local files = require('canola.adapters.files')
local fs = require('canola.fs')
local powershell_trash = require('canola.adapters.trash.windows.powershell-trash')
local FIELD_META = constants.FIELD_META
local FIELD_TYPE = constants.FIELD_TYPE
@ -15,28 +15,28 @@ local M = {}
---@return string
local function get_trash_dir()
local cwd = assert(vim.fn.getcwd())
local trash_dir = cwd:sub(1, 3) .. "$Recycle.Bin"
local trash_dir = cwd:sub(1, 3) .. '$Recycle.Bin'
if vim.fn.isdirectory(trash_dir) == 1 then
return trash_dir
end
trash_dir = "C:\\$Recycle.Bin"
trash_dir = 'C:\\$Recycle.Bin'
if vim.fn.isdirectory(trash_dir) == 1 then
return trash_dir
end
error("No trash found")
error('No trash found')
end
---@param path string
---@return string
local win_addslash = function(path)
if not vim.endswith(path, "\\") then
return path .. "\\"
if not vim.endswith(path, '\\') then
return path .. '\\'
else
return path
end
end
---@class oil.WindowsTrashInfo
---@class canola.WindowsTrashInfo
---@field trash_file string
---@field original_path string
---@field deletion_date integer
@ -44,7 +44,7 @@ end
---@param url string
---@param column_defs string[]
---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
---@param cb fun(err?: string, entries?: canola.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))
@ -61,7 +61,7 @@ M.list = function(url, column_defs, cb)
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 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
@ -70,33 +70,33 @@ M.list = function(url, column_defs, cb)
)
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}}
---@return {[1]:nil, [2]:string, [3]:string, [4]:{stat: uv_fs_t, trash_info: canola.WindowsTrashInfo, display_name: string}}
function(entry)
local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ":h")))
local parent = win_addslash(assert(vim.fn.fnamemodify(entry.OriginalPath, ':h')))
--- @type oil.InternalEntry
--- @type canola.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 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")
cache.create_entry(url, deleted_file_tail, entry.IsFolder and 'directory' or 'file')
-- info_file on windows has the following format: $I<6 char hash>.<extension>
-- the hash is the same for the deleted file and the info file
-- so, we take the hash (and extension) from the deleted file
--
-- see https://superuser.com/questions/368890/how-does-the-recycle-bin-in-windows-work/1736690#1736690
local info_file_tail = deleted_file_tail:match("^%$R(.*)$") --[[@as string?]]
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
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
---@type canola.WindowsTrashInfo
trash_info = {
trash_file = entry.Path,
original_path = entry.OriginalPath,
@ -109,10 +109,10 @@ M.list = function(url, column_defs, cb)
if path ~= parent and (show_all_files or fs.is_subpath(path, parent)) then
local name = parent:sub(path:len() + 1)
local next_par = vim.fs.dirname(name)
while next_par ~= "." do
while next_par ~= '.' do
name = next_par
next_par = vim.fs.dirname(name)
cache_entry = cache.create_entry(url, name, "directory")
cache_entry = cache.create_entry(url, name, 'directory')
cache_entry[FIELD_META] = {}
end
@ -132,7 +132,7 @@ 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")
current_year = vim.fn.strftime('%Y')
end)
local file_columns = {}
@ -142,7 +142,7 @@ file_columns.mtime = {
if not meta then
return nil
end
---@type oil.WindowsTrashInfo
---@type canola.WindowsTrashInfo
local trash_info = meta.trash_info
local time = trash_info and trash_info.deletion_date
if not time then
@ -153,11 +153,11 @@ file_columns.mtime = {
if fmt then
ret = vim.fn.strftime(fmt, time)
else
local year = vim.fn.strftime("%Y", time)
local year = vim.fn.strftime('%Y', time)
if year ~= current_year then
ret = vim.fn.strftime("%b %d %Y", time)
ret = vim.fn.strftime('%b %d %Y', time)
else
ret = vim.fn.strftime("%b %d %H:%M", time)
ret = vim.fn.strftime('%b %d %H:%M', time)
end
end
return ret
@ -165,7 +165,7 @@ file_columns.mtime = {
get_sort_value = function(entry)
local meta = entry[FIELD_META]
---@type nil|oil.WindowsTrashInfo
---@type nil|canola.WindowsTrashInfo
local trash_info = meta and meta.trash_info
if trash_info and trash_info.deletion_date then
return trash_info.deletion_date
@ -178,37 +178,38 @@ file_columns.mtime = {
local fmt = conf and conf.format
local pattern
if fmt then
pattern = fmt:gsub("%%.", "%%S+")
pattern = fmt:gsub('%%.', '%%S+')
else
pattern = "%S+%s+%d+%s+%d%d:?%d%d"
pattern = '%S+%s+%d+%s+%d%d:?%d%d'
end
return line:match("^(" .. pattern .. ")%s+(.+)$")
return line:match('^(' .. pattern .. ')%s+(.+)$')
end,
}
---@param name string
---@return nil|oil.ColumnDefinition
---@return nil|canola.ColumnDefinition
M.get_column = function(name)
return file_columns[name]
end
---@param action oil.Action
---@param action canola.Action
---@return boolean
M.filter_action = function(action)
if action.type == "create" then
if action.type == 'create' then
return false
elseif action.type == "delete" then
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
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
return src_adapter.name == 'files' or dest_adapter.name == 'files'
-- selene: allow(if_same_then_else)
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"
return src_adapter.name == 'files' or dest_adapter.name == 'files'
else
error(string.format("Bad action type '%s'", action.type))
end
@ -219,7 +220,7 @@ end
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")
local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ':p')
assert(os_path)
uv.fs_realpath(
os_path,
@ -231,11 +232,11 @@ M.normalize_url = function(url, callback)
end
---@param url string
---@param entry oil.Entry
---@param entry canola.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 meta = internal_entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: canola.WindowsTrashInfo, display_name: string}]]
local trash_info = meta and meta.trash_info
if not trash_info then
-- This is a subpath in the trash
@ -244,84 +245,84 @@ M.get_entry_path = function(url, entry, cb)
end
local path = fs.os_to_posix_path(trash_info.trash_file)
if entry.type == "directory" then
if entry.type == 'directory' then
path = win_addslash(path)
end
cb("oil://" .. path)
cb('canola://' .. path)
end
---@param err oil.ParseError
---@param err canola.ParseError
---@return boolean
M.filter_error = function(err)
if err.message == "Duplicate filename" then
if err.message == 'Duplicate filename' then
return false
end
return true
end
---@param action oil.Action
---@param action canola.Action
---@return string
M.render_action = function(action)
if action.type == "delete" then
if action.type == 'delete' then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META]
---@type oil.WindowsTrashInfo
---@type canola.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
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
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
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)
return string.format('RESTORE %s', short_path)
else
error("Must be moving files into or out of trash")
error('Must be moving files into or out of trash')
end
elseif action.type == "copy" then
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
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
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)
return string.format('RESTORE %s', short_path)
else
error("Must be copying files into or out of trash")
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[]?)
---@param trash_info canola.WindowsTrashInfo
---@param cb fun(err?: string, raw_entries: canola.WindowsRawEntry[]?)
local purge = function(trash_info, cb)
fs.recursive_delete("file", trash_info.info_file, function(err)
fs.recursive_delete('file', trash_info.info_file, function(err)
if err then
return cb(err)
end
fs.recursive_delete("file", trash_info.trash_file, cb)
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)
---@param cb fun(err?: string, trash_info?: canola.TrashInfo)
local function create_trash_info_and_copy(path, type, cb)
local temp_path = path .. "temp"
local temp_path = path .. 'temp'
-- create a temporary copy on the same location
fs.recursive_copy(
type,
@ -343,28 +344,28 @@ local function create_trash_info_and_copy(path, type, cb)
)
end
---@param action oil.Action
---@param action canola.Action
---@param cb fun(err: nil|string)
M.perform_action = function(action, cb)
if action.type == "delete" then
if action.type == 'delete' then
local entry = assert(cache.get_entry_by_url(action.url))
local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: canola.WindowsTrashInfo, display_name: string}]]
local trash_info = meta and meta.trash_info
purge(trash_info, cb)
elseif action.type == "move" then
elseif action.type == 'move' then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter.name == "files" then
if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url)
M.delete_to_trash(assert(path), cb)
elseif dest_adapter.name == "files" then
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 meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: canola.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
@ -373,33 +374,33 @@ M.perform_action = function(action, cb)
uv.fs_unlink(trash_info.info_file, cb)
end)
end
elseif action.type == "copy" then
elseif action.type == 'copy' then
local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
if src_adapter.name == "files" then
if src_adapter.name == 'files' then
local _, path = util.parse_url(action.src_url)
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
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 meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: canola.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")
error('Must be moving files into or out of trash')
end
else
cb(string.format("Bad action type: %s", action.type))
cb(string.format('Bad action type: %s', action.type))
end
end
M.supported_cross_adapter_actions = { files = "move" }
M.supported_cross_adapter_actions = { files = 'move' }
---@param path string
---@param cb fun(err?: string)

View file

@ -1,18 +1,18 @@
---@class (exact) oil.PowershellCommand
---@class (exact) canola.PowershellCommand
---@field cmd string
---@field cb fun(err?: string, output?: string)
---@field running? boolean
---@class oil.PowershellConnection
---@class canola.PowershellConnection
---@field private jid integer
---@field private execution_error? string
---@field private commands oil.PowershellCommand[]
---@field private commands canola.PowershellCommand[]
---@field private stdout string[]
---@field private is_reading_data boolean
local PowershellConnection = {}
---@param init_command? string
---@return oil.PowershellConnection
---@return canola.PowershellConnection
function PowershellConnection.new(init_command)
local self = setmetatable({
commands = {},
@ -22,7 +22,7 @@ function PowershellConnection.new(init_command)
self:_init(init_command)
---@type oil.PowershellConnection
---@type canola.PowershellConnection
return self
end
@ -41,23 +41,23 @@ function PowershellConnection:_init(init_command)
-- 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",
'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
if fragment:find('===DONE%((%a+)%)===') then
self.is_reading_data = false
local output = table.concat(self.stdout, "")
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
local success = fragment:match('===DONE%((%a+)%)===')
if success == 'True' then
cb(nil, output)
elseif success == "False" then
cb(success .. ": " .. output, output)
elseif success == 'False' then
cb(success .. ': ' .. output, output)
end
self.stdout = {}
self:_consume()

View file

@ -1,7 +1,7 @@
-- A wrapper around trash operations using windows powershell
local Powershell = require("oil.adapters.trash.windows.powershell-connection")
local Powershell = require('canola.adapters.trash.windows.powershell-connection')
---@class oil.WindowsRawEntry
---@class canola.WindowsRawEntry
---@field IsFolder boolean
---@field DeletionDate integer
---@field Name string
@ -30,10 +30,10 @@ $data = @(foreach ($i in $folder.items())
ConvertTo-Json $data -Compress
]]
---@type nil|oil.PowershellConnection
---@type nil|canola.PowershellConnection
local list_entries_powershell
---@param cb fun(err?: string, raw_entries?: oil.WindowsRawEntry[])
---@param cb fun(err?: string, raw_entries?: canola.WindowsRawEntry[])
M.list_raw_entries = function(cb)
if not list_entries_powershell then
list_entries_powershell = Powershell.new(list_entries_init)
@ -63,7 +63,7 @@ $path = Get-Item '%s'
$folder.ParseName($path.FullName).InvokeVerb('delete')
]]
---@type nil|oil.PowershellConnection
---@type nil|canola.PowershellConnection
local delete_to_trash_powershell
---@param path string

View file

@ -1,5 +1,5 @@
local constants = require("oil.constants")
local util = require("oil.util")
local constants = require('canola.constants')
local util = require('canola.util')
local M = {}
local FIELD_ID = constants.FIELD_ID
@ -8,11 +8,11 @@ local FIELD_META = constants.FIELD_META
local next_id = 1
-- Map<url, Map<entry name, oil.InternalEntry>>
---@type table<string, table<string, oil.InternalEntry>>
-- Map<url, Map<entry name, canola.InternalEntry>>
---@type table<string, table<string, canola.InternalEntry>>
local url_directory = {}
---@type table<integer, oil.InternalEntry>
---@type table<integer, canola.InternalEntry>
local entries_by_id = {}
---@type table<integer, string>
@ -28,7 +28,7 @@ local _cached_id_fmt
M.format_id = function(id)
if not _cached_id_fmt then
local id_str_length = math.max(3, 1 + math.floor(math.log10(next_id)))
_cached_id_fmt = "/%0" .. string.format("%d", id_str_length) .. "d"
_cached_id_fmt = '/%0' .. string.format('%d', id_str_length) .. 'd'
end
return _cached_id_fmt:format(id)
end
@ -42,8 +42,8 @@ end
---@param parent_url string
---@param name string
---@param type oil.EntryType
---@return oil.InternalEntry
---@param type canola.EntryType
---@return canola.InternalEntry
M.create_entry = function(parent_url, name, type)
parent_url = util.addslash(parent_url)
local parent = tmp_url_directory[parent_url] or url_directory[parent_url]
@ -58,7 +58,7 @@ M.create_entry = function(parent_url, name, type)
end
---@param parent_url string
---@param entry oil.InternalEntry
---@param entry canola.InternalEntry
M.store_entry = function(parent_url, entry)
parent_url = util.addslash(parent_url)
local parent = url_directory[parent_url]
@ -85,8 +85,8 @@ end
---@param parent_url string
---@param name string
---@param type oil.EntryType
---@return oil.InternalEntry
---@param type canola.EntryType
---@return canola.InternalEntry
M.create_and_store_entry = function(parent_url, name, type)
local entry = M.create_entry(parent_url, name, type)
M.store_entry(parent_url, entry)
@ -115,18 +115,18 @@ M.end_update_url = function(parent_url)
end
---@param id integer
---@return nil|oil.InternalEntry
---@return nil|canola.InternalEntry
M.get_entry_by_id = function(id)
return entries_by_id[id]
end
---@param url string
---@return nil|oil.InternalEntry
---@return nil|canola.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")
local parent_url = scheme .. vim.fn.fnamemodify(path, ':h')
local basename = vim.fn.fnamemodify(path, ':t')
return M.list_url(parent_url)[basename]
end
@ -135,46 +135,46 @@ end
M.get_parent_url = function(id)
local url = parent_url_by_id[id]
if not url then
error(string.format("Entry %d missing parent url", id))
error(string.format('Entry %d missing parent url', id))
end
return url
end
---@param url string
---@return table<string, oil.InternalEntry>
---@return table<string, canola.InternalEntry>
M.list_url = function(url)
url = util.addslash(url)
return url_directory[url] or {}
end
---@param action oil.Action
---@param action canola.Action
M.perform_action = function(action)
if action.type == "create" then
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")
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
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 parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ':h'))
local name = vim.fn.fnamemodify(path, ':t')
local entry = url_directory[parent_url][name]
url_directory[parent_url][name] = nil
entries_by_id[entry[FIELD_ID]] = nil
parent_url_by_id[entry[FIELD_ID]] = nil
elseif action.type == "move" then
elseif action.type == 'move' then
local src_scheme, src_path = util.parse_url(action.src_url)
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 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")
local dest_parent_url = util.addslash(dest_scheme .. vim.fn.fnamemodify(dest_path, ':h'))
local dest_name = vim.fn.fnamemodify(dest_path, ':t')
url_directory[src_parent_url][src_name] = nil
local dest_parent = url_directory[dest_parent_url]
@ -188,13 +188,14 @@ M.perform_action = function(action)
parent_url_by_id[entry[FIELD_ID]] = dest_parent_url
entry[FIELD_NAME] = dest_name
util.update_moved_buffers(action.entry_type, action.src_url, action.dest_url)
elseif action.type == "copy" then
elseif action.type == 'copy' then
local scheme, path = util.parse_url(action.dest_url)
assert(path)
local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h"))
local name = vim.fn.fnamemodify(path, ":t")
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
-- selene: allow(empty_if)
elseif action.type == 'change' then
-- Cache doesn't need to update
else
---@diagnostic disable-next-line: undefined-field

View file

@ -1,11 +1,11 @@
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 cache = require('canola.cache')
local canola = require('canola')
local columns = require('canola.columns')
local config = require('canola.config')
local fs = require('canola.fs')
local parser = require('canola.mutator.parser')
local util = require('canola.util')
local view = require('canola.view')
local M = {}
@ -16,10 +16,10 @@ local function get_linux_session_type()
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"
if xdg_session_type:find('x11') then
return 'x11'
elseif xdg_session_type:find('wayland') then
return 'wayland'
else
return nil
end
@ -29,15 +29,15 @@ end
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"
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 entry canola.InternalEntry
---@param column_defs canola.ColumnSpec[]
---@param adapter canola.Adapter
---@param bufnr integer
local function write_pasted(winid, entry, column_defs, adapter, bufnr)
local col_width = {}
@ -52,10 +52,10 @@ local function write_pasted(winid, entry, column_defs, adapter, bufnr)
end
---@param parent_url string
---@param entry oil.InternalEntry
---@param entry canola.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")
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)
@ -77,7 +77,7 @@ end
---@param delete_original? boolean
local function paste_paths(paths, delete_original)
local bufnr = vim.api.nvim_get_current_buf()
local scheme = "oil://"
local scheme = 'canola://'
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()
@ -88,7 +88,7 @@ local function paste_paths(paths, delete_original)
-- Handle as many paths synchronously as possible
for _, path in ipairs(paths) do
-- Trim the trailing slash off directories
if vim.endswith(path, "/") then
if vim.endswith(path, '/') then
path = path:sub(1, -2)
end
@ -114,7 +114,7 @@ local function paste_paths(paths, delete_original)
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)
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)
@ -140,7 +140,7 @@ local function paste_paths(paths, delete_original)
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)
canola.load_canola_buffer(new_bufnr)
util.run_after_load(new_bufnr, complete_loading)
end
end
@ -149,8 +149,8 @@ end
---@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 = vim.fn.getpos('v')
local end_ = vim.fn.getpos('.')
local start_row = start[2]
local end_row = end_[2]
@ -162,38 +162,38 @@ local function range_from_selection()
end
M.copy_to_system_clipboard = function()
local dir = oil.get_current_dir()
local dir = canola.get_current_dir()
if not dir then
vim.notify("System clipboard only works for local files", vim.log.levels.ERROR)
vim.notify('System clipboard only works for local files', vim.log.levels.ERROR)
return
end
local entries = {}
local mode = vim.api.nvim_get_mode().mode
if mode == "v" or mode == "V" then
if mode == 'v' or mode == 'V' then
if fs.is_mac then
vim.notify(
"Copying multiple paths to clipboard is not supported on mac",
'Copying multiple paths to clipboard is not supported on mac',
vim.log.levels.ERROR
)
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))
table.insert(entries, canola.get_entry_on_line(0, i))
end
-- leave visual mode
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", true, false, true), "n", true)
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<Esc>', true, false, true), 'n', true)
else
table.insert(entries, oil.get_cursor_entry())
table.insert(entries, canola.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)
vim.notify('Could not find local file under cursor', vim.log.levels.WARN)
return
end
local paths = {}
@ -204,38 +204,38 @@ M.copy_to_system_clipboard = function()
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",
'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")
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)
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)
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" })
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" })
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)
vim.notify('System clipboard not supported on Windows', vim.log.levels.ERROR)
return
end
@ -243,11 +243,11 @@ M.copy_to_system_clipboard = function()
vim.notify(string.format("Could not find executable '%s'", cmd[1]), vim.log.levels.ERROR)
return
end
local stderr = ""
local stderr = ''
local jid = vim.fn.jobstart(cmd, {
stderr_buffered = true,
on_stderr = function(_, data)
stderr = table.concat(data, "\n")
stderr = table.concat(data, '\n')
end,
on_exit = function(j, exit_code)
if exit_code ~= 0 then
@ -259,15 +259,15 @@ M.copy_to_system_clipboard = function()
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))
vim.notify(string.format('Copied %d files to system clipboard', #paths))
end
end
end,
})
assert(jid > 0, "Failed to start job")
assert(jid > 0, 'Failed to start job')
if stdin then
vim.api.nvim_chan_send(jid, stdin)
vim.fn.chanclose(jid, "stdin")
vim.fn.chanclose(jid, 'stdin')
end
end
@ -276,7 +276,7 @@ end
local function handle_paste_output_mac(lines)
local ret = {}
for _, line in ipairs(lines) do
if not line:match("^%s*$") then
if not line:match('^%s*$') then
table.insert(ret, line)
end
end
@ -288,7 +288,7 @@ end
local function handle_paste_output_linux(lines)
local ret = {}
for _, line in ipairs(lines) do
local path = line:match("^file://(.+)$")
local path = line:match('^file://(.+)$')
if path then
table.insert(ret, util.url_unescape(path))
end
@ -298,7 +298,7 @@ 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()
local dir = canola.get_current_dir()
if not dir then
return
end
@ -306,37 +306,37 @@ M.paste_from_system_clipboard = function(delete_original)
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",
'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")
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)
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" })
vim.list_extend(cmd, { '-t', 'x-special/gnome-copied-files' })
else
vim.list_extend(cmd, { "-t", "text/uri-list" })
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)
vim.notify('System clipboard not supported on Windows', vim.log.levels.ERROR)
return
end
local paths
local stderr = ""
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
@ -345,26 +345,26 @@ M.paste_from_system_clipboard = function(delete_original)
stdout_buffered = true,
stderr_buffered = true,
on_stdout = function(j, data)
local lines = vim.split(table.concat(data, "\n"), "\r?\n")
local lines = vim.split(table.concat(data, '\n'), '\r?\n')
paths = handle_paste_output(lines)
end,
on_stderr = function(_, data)
stderr = table.concat(data, "\n")
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),
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)
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")
assert(jid > 0, 'Failed to start job')
end
return M

View file

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

View file

@ -1,41 +1,45 @@
local default_config = {
-- Oil will take over directory buffers (e.g. `vim .` or `:e src/`)
-- Canola 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
-- See :help canola-columns
columns = {
"icon",
'icon',
-- "permissions",
-- "size",
-- "mtime",
},
-- Buffer-local options to use for oil buffers
-- Buffer-local options to use for canola buffers
buf_options = {
buflisted = false,
bufhidden = "hide",
bufhidden = 'hide',
},
-- Window-local options to use for oil buffers
-- Window-local options to use for canola buffers
win_options = {
wrap = false,
signcolumn = "no",
signcolumn = 'no',
cursorcolumn = false,
foldcolumn = "0",
foldcolumn = '0',
spell = false,
list = false,
conceallevel = 3,
concealcursor = "nvic",
concealcursor = 'nvic',
},
-- Send deleted files to the trash instead of permanently deleting them (:help oil-trash)
-- Send deleted files to the trash instead of permanently deleting them (:help canola-trash)
delete_to_trash = false,
-- Skip the confirmation popup for simple operations (:help oil.skip_confirm_for_simple_edits)
-- Wipe open buffers for files deleted via canola (:help canola.cleanup_buffers_on_delete)
cleanup_buffers_on_delete = false,
-- Skip the confirmation popup for simple operations (:help canola.skip_confirm_for_simple_edits)
skip_confirm_for_simple_edits = false,
skip_confirm_for_delete = false,
-- 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
auto_save_on_select_new_entry = false,
-- Canola 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
-- Note that the cleanup process only starts when none of the canola buffers are currently displayed
cleanup_delay_ms = 2000,
lsp_file_methods = {
-- Enable or disable LSP file operations
@ -46,43 +50,44 @@ local default_config = {
-- Set to "unmodified" to only save unmodified buffers
autosave_changes = false,
},
-- Constrain the cursor to the editable parts of the oil buffer
-- Constrain the cursor to the editable parts of the canola 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
constrain_cursor = 'editable',
-- Set to true to watch the filesystem for changes and reload canola
watch_for_changes = false,
-- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap
-- Keymaps in canola 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" })
-- Additionally, if it is a string that matches "actions.<name>",
-- it will use the mapping at require("oil.actions").<name>
-- it will use the mapping at require("canola.actions").<name>
-- Set to `false` to remove a keymap
-- See :help oil-actions for a list of all available actions
-- See :help canola-actions for a list of all available actions
keymaps = {
["g?"] = { "actions.show_help", mode = "n" },
["<CR>"] = "actions.select",
["<C-s>"] = { "actions.select", opts = { vertical = true } },
["<C-h>"] = { "actions.select", opts = { horizontal = true } },
["<C-t>"] = { "actions.select", opts = { tab = true } },
["<C-p>"] = "actions.preview",
["<C-c>"] = { "actions.close", mode = "n" },
["<C-l>"] = "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" },
['g?'] = { 'actions.show_help', mode = 'n' },
['<CR>'] = 'actions.select',
['<C-s>'] = { 'actions.select', opts = { vertical = true } },
['<C-h>'] = { 'actions.select', opts = { horizontal = true } },
['<C-t>'] = { 'actions.select', opts = { tab = true } },
['<C-p>'] = 'actions.preview',
['<C-c>'] = { 'actions.close', mode = 'n' },
['<C-l>'] = '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' },
},
-- Set to false to disable all of the above keymaps
use_default_keymaps = true,
view_options = {
-- Show files and directories that start with "."
show_hidden = false,
show_hidden_when_empty = false,
-- This function defines what is considered a "hidden" file
is_hidden_file = function(name, bufnr)
local m = name:match("^%.")
local m = name:match('^%.')
return m ~= nil
end,
-- This function defines what will never be shown, even when `show_hidden` is set
@ -91,14 +96,14 @@ local default_config = {
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",
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" },
-- see :help canola-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)
@ -124,7 +129,7 @@ local default_config = {
return false
end,
},
-- Configuration for the floating window in oil.open_float
-- Configuration for the floating window in canola.open_float
float = {
-- Padding around the floating window
padding = 2,
@ -135,10 +140,10 @@ local default_config = {
win_options = {
winblend = 0,
},
-- optionally override the oil buffers window title with custom function: fun(winid: integer): string
-- optionally override the canola 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",
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)
@ -150,11 +155,12 @@ local default_config = {
-- 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",
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,
max_file_size = 10,
-- Window-local options to use for preview window buffers
win_options = {},
},
@ -190,7 +196,7 @@ local default_config = {
min_height = { 5, 0.1 },
height = nil,
border = nil,
minimized_border = "none",
minimized_border = 'none',
win_options = {
winblend = 0,
},
@ -209,177 +215,187 @@ local default_config = {
-- 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
-- not "canola-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://"
local canola_s3_string = vim.fn.has('nvim-0.12') == 1 and 'canola-s3://' or 'canola-sss://'
default_config.adapters = {
["oil://"] = "files",
["oil-ssh://"] = "ssh",
[oil_s3_string] = "s3",
["oil-trash://"] = "trash",
['canola://'] = 'files',
['canola-ssh://'] = 'ssh',
[canola_s3_string] = 's3',
['canola-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
---@class canola.Config
---@field adapters table<string, string> Hidden from SetupOpts
---@field adapter_aliases table<string, string> Hidden from SetupOpts
---@field silence_scp_warning? boolean Undocumented option
---@field default_file_explorer boolean
---@field columns oil.ColumnSpec[]
---@field columns canola.ColumnSpec[]
---@field buf_options table<string, any>
---@field win_options table<string, any>
---@field delete_to_trash boolean
---@field cleanup_buffers_on_delete boolean
---@field skip_confirm_for_simple_edits boolean
---@field skip_confirm_for_delete boolean
---@field prompt_save_on_select_new_entry boolean
---@field auto_save_on_select_new_entry boolean
---@field cleanup_delay_ms integer
---@field lsp_file_methods oil.LspFileMethods
---@field lsp_file_methods canola.LspFileMethods
---@field constrain_cursor false|"name"|"editable"
---@field watch_for_changes boolean
---@field keymaps table<string, any>
---@field use_default_keymaps boolean
---@field view_options oil.ViewOptions
---@field view_options canola.ViewOptions
---@field new_file_mode integer
---@field new_dir_mode integer
---@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
---@field git canola.GitOptions
---@field float canola.FloatWindowConfig
---@field preview_win canola.PreviewWindowConfig
---@field confirmation canola.ConfirmationWindowConfig
---@field progress canola.ProgressWindowConfig
---@field ssh canola.SimpleWindowConfig
---@field keymaps_help canola.SimpleWindowConfig
local M = {}
-- For backwards compatibility
---@alias oil.setupOpts oil.SetupOpts
---@alias canola.setupOpts canola.SetupOpts
---@class (exact) oil.SetupOpts
---@field default_file_explorer? boolean Oil will take over directory buffers (e.g. `vim .` or `:e src/`). Set to false if you still want to use netrw.
---@field columns? oil.ColumnSpec[] The columns to display. See :help oil-columns.
---@field buf_options? table<string, any> Buffer-local options to use for oil buffers
---@field win_options? table<string, any> Window-local options to use for oil buffers
---@field delete_to_trash? boolean Send deleted files to the trash instead of permanently deleting them (:help oil-trash).
---@field skip_confirm_for_simple_edits? boolean Skip the confirmation popup for simple operations (:help oil.skip_confirm_for_simple_edits).
---@class (exact) canola.SetupOpts
---@field default_file_explorer? boolean Canola will take over directory buffers (e.g. `vim .` or `:e src/`). Set to false if you still want to use netrw.
---@field columns? canola.ColumnSpec[] The columns to display. See :help canola-columns.
---@field buf_options? table<string, any> Buffer-local options to use for canola buffers
---@field win_options? table<string, any> Window-local options to use for canola buffers
---@field delete_to_trash? boolean Send deleted files to the trash instead of permanently deleting them (:help canola-trash).
---@field cleanup_buffers_on_delete? boolean Wipe open buffers for files deleted via canola (:help canola.cleanup_buffers_on_delete).
---@field skip_confirm_for_simple_edits? boolean Skip the confirmation popup for simple operations (:help canola.skip_confirm_for_simple_edits).
---@field skip_confirm_for_delete? boolean Skip the confirmation popup when all pending actions are deletes (:help canola.skip_confirm_for_delete).
---@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 auto_save_on_select_new_entry? boolean Automatically save changes when selecting a new/moved/renamed entry, instead of prompting (:help canola.auto_save_on_select_new_entry).
---@field cleanup_delay_ms? integer Canola 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 canola buffers are currently displayed.
---@field lsp_file_methods? canola.SetupLspFileMethods Configure LSP file operation integration.
---@field constrain_cursor? false|"name"|"editable" Constrain the cursor to the editable parts of the canola 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 canola.
---@field keymaps? table<string, any>
---@field use_default_keymaps? boolean Set to false to disable all of the above keymaps
---@field view_options? oil.SetupViewOptions Configure which files are shown and how they are shown.
---@field view_options? canola.SetupViewOptions Configure which files are shown and how they are shown.
---@field new_file_mode? integer Permission mode for new files in decimal (default 420 = 0644)
---@field new_dir_mode? integer Permission mode for new directories in decimal (default 493 = 0755)
---@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
---@field git? canola.SetupGitOptions EXPERIMENTAL support for performing file operations with git
---@field float? canola.SetupFloatWindowConfig Configuration for the floating window in canola.open_float
---@field preview_win? canola.SetupPreviewWindowConfig Configuration for the file preview window
---@field confirmation? canola.SetupConfirmationWindowConfig Configuration for the floating action confirmation window
---@field progress? canola.SetupProgressWindowConfig Configuration for the floating progress window
---@field ssh? canola.SetupSimpleWindowConfig Configuration for the floating SSH window
---@field keymaps_help? canola.SetupSimpleWindowConfig Configuration for the floating keymaps help window
---@class (exact) oil.LspFileMethods
---@class (exact) canola.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
---@class (exact) canola.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
---@class (exact) canola.ViewOptions
---@field show_hidden boolean
---@field is_hidden_file fun(name: string, bufnr: integer, entry: oil.Entry): boolean
---@field is_always_hidden fun(name: string, bufnr: integer, entry: oil.Entry): boolean
---@field show_hidden_when_empty boolean
---@field is_hidden_file fun(name: string, bufnr: integer, entry: canola.Entry): boolean
---@field is_always_hidden fun(name: string, bufnr: integer, entry: canola.Entry): 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
---@field sort canola.SortSpec[]
---@field highlight_filename? fun(entry: canola.Entry, is_hidden: boolean, is_link_target: boolean, is_link_orphan: boolean, bufnr: integer): string|nil
---@class (exact) oil.SetupViewOptions
---@class (exact) canola.SetupViewOptions
---@field show_hidden? boolean Show files and directories that start with "."
---@field show_hidden_when_empty? boolean When true and the directory has no visible entries, show hidden entries instead of an empty listing (:help canola.show_hidden_when_empty).
---@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
---@field sort? canola.SortSpec[] Sort order for the file list
---@field highlight_filename? fun(entry: canola.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
---@class (exact) canola.SortSpec
---@field [1] string
---@field [2] "asc"|"desc"
---@class (exact) oil.GitOptions
---@class (exact) canola.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
---@class (exact) canola.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
---@class (exact) canola.WindowDimensionDualConstraint
---@field [1] number
---@field [2] number
---@alias oil.WindowDimension number|oil.WindowDimensionDualConstraint
---@alias canola.WindowDimension number|canola.WindowDimensionDualConstraint
---@class (exact) oil.WindowConfig
---@field max_width oil.WindowDimension
---@field min_width oil.WindowDimension
---@class (exact) canola.WindowConfig
---@field max_width canola.WindowDimension
---@field min_width canola.WindowDimension
---@field width? number
---@field max_height oil.WindowDimension
---@field min_height oil.WindowDimension
---@field max_height canola.WindowDimension
---@field min_height canola.WindowDimension
---@field height? number
---@field border string|string[]
---@field win_options table<string, any>
---@class (exact) oil.SetupWindowConfig
---@field max_width? oil.WindowDimension Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total"
---@field min_width? oil.WindowDimension Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. min_width = {40, 0.4} means "the greater of 40 columns or 40% of total"
---@class (exact) canola.SetupWindowConfig
---@field max_width? canola.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? canola.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 max_height? canola.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? canola.WindowDimension Height dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. min_height = {5, 0.1} means "the greater of 5 columns or 10% of total"
---@field height? number Define an integer/float for the exact height of the preview window
---@field border? string|string[] Window border
---@field win_options? table<string, any>
---@alias oil.PreviewMethod
---@alias canola.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
---@class (exact) canola.PreviewWindowConfig
---@field update_on_cursor_moved boolean
---@field preview_method oil.PreviewMethod
---@field preview_method canola.PreviewMethod
---@field disable_preview fun(filename: string): boolean
---@field max_file_size number Maximum file size (in MB) to preview. Files larger than this will show a placeholder.
---@field win_options table<string, any>
---@class (exact) oil.ConfirmationWindowConfig : oil.WindowConfig
---@class (exact) canola.ConfirmationWindowConfig : canola.WindowConfig
---@class (exact) oil.SetupPreviewWindowConfig
---@class (exact) canola.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 max_file_size? number Maximum file size in MB to show in preview. Files exceeding this will not be loaded (:help canola.preview_win). Set to nil to disable the limit.
---@field preview_method? canola.PreviewMethod How to open the preview window
---@field win_options? table<string, any> Window-local options to use for preview window buffers
---@class (exact) oil.SetupConfirmationWindowConfig : oil.SetupWindowConfig
---@class (exact) canola.SetupConfirmationWindowConfig : canola.SetupWindowConfig
---@class (exact) oil.ProgressWindowConfig : oil.WindowConfig
---@class (exact) canola.ProgressWindowConfig : canola.WindowConfig
---@field minimized_border string|string[]
---@class (exact) oil.SetupProgressWindowConfig : oil.SetupWindowConfig
---@class (exact) canola.SetupProgressWindowConfig : canola.SetupWindowConfig
---@field minimized_border? string|string[] The border for the minimized progress window
---@class (exact) oil.FloatWindowConfig
---@class (exact) canola.FloatWindowConfig
---@field padding integer
---@field max_width integer
---@field max_height integer
@ -389,7 +405,7 @@ local M = {}
---@field preview_split "auto"|"left"|"right"|"above"|"below"
---@field override fun(conf: table): table
---@class (exact) oil.SetupFloatWindowConfig
---@class (exact) canola.SetupFloatWindowConfig
---@field padding? integer
---@field max_width? integer
---@field max_height? integer
@ -399,16 +415,16 @@ local M = {}
---@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
---@class (exact) canola.SimpleWindowConfig
---@field border string|string[]
---@class (exact) oil.SetupSimpleWindowConfig
---@class (exact) canola.SetupSimpleWindowConfig
---@field border? string|string[] Window border
M.setup = function(opts)
opts = opts or vim.g.oil or {}
opts = opts or vim.g.canola or {}
local new_conf = vim.tbl_deep_extend("keep", opts, default_config)
local new_conf = vim.tbl_deep_extend('keep', opts, default_config)
if not new_conf.use_default_keymaps then
new_conf.keymaps = opts.keymaps or {}
elseif opts.keymaps then
@ -429,19 +445,19 @@ M.setup = function(opts)
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" },
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)
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
@ -452,7 +468,7 @@ M.setup = function(opts)
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.",
'canola 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
@ -474,15 +490,15 @@ M.setup = function(opts)
end
---@param scheme nil|string
---@return nil|oil.Adapter
---@return nil|canola.Adapter
M.get_adapter_by_scheme = function(scheme)
if not scheme then
return nil
end
if not vim.endswith(scheme, "://") then
local pieces = vim.split(scheme, "://", { plain = true })
if not vim.endswith(scheme, '://') then
local pieces = vim.split(scheme, '://', { plain = true })
if #pieces <= 2 then
scheme = pieces[1] .. "://"
scheme = pieces[1] .. '://'
else
error(string.format("Malformed url: '%s'", scheme))
end
@ -494,7 +510,7 @@ M.get_adapter_by_scheme = function(scheme)
return nil
end
local ok
ok, adapter = pcall(require, string.format("oil.adapters.%s", name))
ok, adapter = pcall(require, string.format('canola.adapters.%s', name))
if ok then
adapter.name = name
M._adapter_by_scheme[scheme] = adapter

View file

@ -2,9 +2,9 @@ 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 canola.InternalEntry {[1]: integer, [2]: string, [3]: canola.EntryType, [4]: nil|table}
-- Indexes into oil.InternalEntry
-- Indexes into canola.InternalEntry
M.FIELD_ID = 1
M.FIELD_NAME = 2
M.FIELD_TYPE = 3

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -43,7 +43,7 @@ local function calc_list(values, max_value, aggregator, limit)
local ret = limit
if not max_value or not values then
return nil
elseif type(values) == "table" then
elseif type(values) == 'table' then
for _, v in ipairs(values) do
ret = aggregator(ret, calc_float(v, max_value))
end
@ -98,7 +98,7 @@ M.calculate_height = function(desired_height, opts)
)
end
---@class (exact) oil.WinLayout
---@class (exact) canola.WinLayout
---@field width integer
---@field height integer
---@field row integer
@ -106,12 +106,12 @@ end
---@return vim.api.keyset.win_config
M.get_fullscreen_win_opts = function()
local config = require("oil.config")
local config = require('canola.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
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
@ -127,7 +127,7 @@ M.get_fullscreen_win_opts = function()
local col = math.floor((total_width - width) / 2) - 1 -- adjust for border width
local win_opts = {
relative = "editor",
relative = 'editor',
width = width,
height = height,
row = row,
@ -141,29 +141,29 @@ 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
---@return canola.WinLayout root_dim New dimensions of the original window
---@return canola.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"
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
---@type canola.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
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
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
@ -171,13 +171,13 @@ M.split_window = function(winid, direction, gap)
dim_root.height = dim_new.height
end
if direction == "left" then
if direction == 'left' then
dim_root.col = dim_root.col + dim_root.width + gap
elseif direction == "right" then
elseif direction == 'right' then
dim_new.col = dim_new.col + dim_new.width + gap
elseif direction == "above" then
elseif direction == 'above' then
dim_root.row = dim_root.row + dim_root.height + gap
elseif direction == "below" then
elseif direction == 'below' then
dim_new.row = dim_new.row + dim_new.height + gap
end

View file

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

View file

@ -11,14 +11,14 @@ Log.level = vim.log.levels.WARN
---@return string
Log.get_logfile = function()
local fs = require("oil.fs")
local fs = require('canola.fs')
local ok, stdpath = pcall(vim.fn.stdpath, "log")
local ok, stdpath = pcall(vim.fn.stdpath, 'log')
if not ok then
stdpath = vim.fn.stdpath("cache")
stdpath = vim.fn.stdpath('cache')
end
assert(type(stdpath) == "string")
return fs.join(stdpath, "oil.log")
assert(type(stdpath) == 'string')
return fs.join(stdpath, 'canola.log')
end
---@param level integer
@ -29,19 +29,19 @@ 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
if type(v) == 'table' then
args[i] = vim.inspect(v)
elseif v == nil then
args[i] = "nil"
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 = ""
local timestr = ''
if ok then
local str_level = levels_reverse[level]
return string.format("%s[%s] %s", timestr, str_level, text)
return string.format('%s[%s] %s', timestr, str_level, text)
else
return string.format(
"%s[ERROR] error formatting log line: '%s' args %s",
@ -67,22 +67,22 @@ local function initialize()
local stat = uv.fs_stat(filepath)
if stat and stat.size > 10 * 1024 * 1024 then
local backup = filepath .. ".1"
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)
require('canola.fs').mkdirp(parent)
local logfile, openerr = io.open(filepath, "a+")
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)
local err_msg = string.format('Failed to open canola.nvim log file: %s', openerr)
vim.notify(err_msg, vim.log.levels.ERROR)
else
write = function(line)
logfile:write(line)
logfile:write("\n")
logfile:write('\n')
logfile:flush()
end
end

View file

@ -1,18 +1,18 @@
local config = require("oil.config")
local fs = require("oil.fs")
local util = require("oil.util")
local workspace = require("oil.lsp.workspace")
local config = require('canola.config')
local fs = require('canola.fs')
local util = require('canola.util')
local workspace = require('canola.lsp.workspace')
local M = {}
---@param actions oil.Action[]
---@param actions canola.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
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))
@ -20,32 +20,32 @@ M.will_perform_file_operations = function(actions)
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
if src_adapter.name == 'files' and dest_adapter.name == 'files' then
moves[src_path] = dest_path
elseif src_adapter.name == "files" then
elseif src_adapter.name == 'files' then
table.insert(deletes, src_path)
elseif dest_adapter.name == "files" then
elseif dest_adapter.name == 'files' then
table.insert(creates, src_path)
end
elseif action.type == "create" then
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
if adapter.name == 'files' then
table.insert(creates, path)
end
elseif action.type == "delete" then
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
if adapter.name == 'files' then
table.insert(deletes, path)
end
elseif action.type == "copy" then
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
if adapter.name == 'files' then
table.insert(creates, path)
end
end
@ -84,7 +84,7 @@ M.will_perform_file_operations = function(actions)
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)),
string.format('[lsp] file operation error: %s', vim.inspect(final_err)),
vim.log.levels.WARN
)
end
@ -102,7 +102,7 @@ M.will_perform_file_operations = function(actions)
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)
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()

View file

@ -1,13 +1,13 @@
local fs = require("oil.fs")
local ms = require("vim.lsp.protocol").Methods
if vim.fn.has("nvim-0.10") == 0 then
local fs = require('canola.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",
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
@ -16,7 +16,7 @@ local M = {}
---@param method string
---@return vim.lsp.Client[]
local function get_clients(method)
if vim.fn.has("nvim-0.10") == 1 then
if vim.fn.has('nvim-0.10') == 1 then
return vim.lsp.get_clients({ method = method })
else
---@diagnostic disable-next-line: deprecated
@ -32,7 +32,7 @@ end
---@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
if type(glob) ~= 'string' then
return glob:match(path) ~= nil
end
@ -59,7 +59,7 @@ local function get_matching_paths(client, filters, paths)
local match_fns = {}
for _, filter in ipairs(filters) do
if filter.scheme == nil or filter.scheme == "file" then
if filter.scheme == nil or filter.scheme == 'file' then
local pattern = filter.pattern
local glob = pattern.glob
local ignore_case = pattern.options and pattern.options.ignoreCase
@ -69,32 +69,32 @@ local function get_matching_paths(client, filters, paths)
-- Some language servers use forward slashes as path separators on Windows (LuaLS)
-- 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("/", "\\")
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, ",")
glob = glob:gsub('{(.-)}', function(s)
local patterns = vim.split(s, ',')
local filtered = {}
for _, pat in ipairs(patterns) do
if pat ~= "" then
if pat ~= '' then
table.insert(filtered, pat)
end
end
if #filtered == 0 then
return ""
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
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, ",") .. "}"
return '{' .. table.concat(filtered, ',') .. '}'
end)
glob_to_match = vim.glob.to_lpeg(glob)
@ -102,7 +102,7 @@ local function get_matching_paths(client, filters, paths)
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
if matches and ((matches == 'file' and is_dir) or (matches == 'folder' and not is_dir)) then
return false
end
@ -163,10 +163,10 @@ local function will_file_operation(method, capability_name, files, options)
for _, client in ipairs(clients) do
local filters = vim.tbl_get(
client.server_capabilities,
"workspace",
"fileOperations",
'workspace',
'fileOperations',
capability_name,
"filters"
'filters'
)
local matching_files = get_matching_paths(client, filters, files)
if matching_files then
@ -178,7 +178,7 @@ local function will_file_operation(method, capability_name, files, options)
end, matching_files),
}
local result, err
if vim.fn.has("nvim-0.11") == 1 then
if vim.fn.has('nvim-0.11') == 1 then
result, err = client:request_sync(method, params, options.timeout_ms or 1000, 0)
else
---@diagnostic disable-next-line: param-type-mismatch
@ -205,10 +205,10 @@ local function did_file_operation(method, capability_name, files)
for _, client in ipairs(clients) do
local filters = vim.tbl_get(
client.server_capabilities,
"workspace",
"fileOperations",
'workspace',
'fileOperations',
capability_name,
"filters"
'filters'
)
local matching_files = get_matching_paths(client, filters, files)
if matching_files then
@ -219,7 +219,7 @@ local function did_file_operation(method, capability_name, files)
}
end, matching_files),
}
if vim.fn.has("nvim-0.11") == 1 then
if vim.fn.has('nvim-0.11') == 1 then
client:notify(method, params)
else
---@diagnostic disable-next-line: param-type-mismatch
@ -239,13 +239,13 @@ end
---@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)
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)
did_file_operation(ms.workspace_didCreateFiles, 'didCreate', files)
end
--- Notify the server that the client is about to delete files.
@ -258,13 +258,13 @@ end
---@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[]
---@return nil|string|lsp.ResponseError err
function M.will_delete_files(files, options)
return will_file_operation(ms.workspace_willDeleteFiles, "willDelete", files, options)
return will_file_operation(ms.workspace_willDeleteFiles, 'willDelete', files, options)
end
--- 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)
did_file_operation(ms.workspace_didDeleteFiles, 'didDelete', files)
end
--- Notify the server that the client is about to rename files.
@ -284,10 +284,10 @@ function M.will_rename_files(files, options)
for _, client in ipairs(clients) do
local filters = vim.tbl_get(
client.server_capabilities,
"workspace",
"fileOperations",
"willRename",
"filters"
'workspace',
'fileOperations',
'willRename',
'filters'
)
local matching_files = get_matching_paths(client, filters, vim.tbl_keys(files))
if matching_files then
@ -300,7 +300,7 @@ function M.will_rename_files(files, options)
end, matching_files),
}
local result, err
if vim.fn.has("nvim-0.11") == 1 then
if vim.fn.has('nvim-0.11') == 1 then
result, err =
client:request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0)
else
@ -327,7 +327,7 @@ 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")
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 = {
@ -338,7 +338,7 @@ function M.did_rename_files(files)
}
end, matching_files),
}
if vim.fn.has("nvim-0.11") == 1 then
if vim.fn.has('nvim-0.11') == 1 then
client:notify(ms.workspace_didRenameFiles, params)
else
---@diagnostic disable-next-line: param-type-mismatch

View file

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

View file

@ -1,60 +1,60 @@
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 util = require("oil.util")
local view = require("oil.view")
local Progress = require('canola.mutator.progress')
local Trie = require('canola.mutator.trie')
local cache = require('canola.cache')
local canola = require('canola')
local columns = require('canola.columns')
local config = require('canola.config')
local confirmation = require('canola.mutator.confirmation')
local constants = require('canola.constants')
local fs = require('canola.fs')
local lsp_helpers = require('canola.lsp.helpers')
local parser = require('canola.mutator.parser')
local util = require('canola.util')
local view = require('canola.view')
local M = {}
local FIELD_NAME = constants.FIELD_NAME
local FIELD_TYPE = constants.FIELD_TYPE
---@alias oil.Action oil.CreateAction|oil.DeleteAction|oil.MoveAction|oil.CopyAction|oil.ChangeAction
---@alias canola.Action canola.CreateAction|canola.DeleteAction|canola.MoveAction|canola.CopyAction|canola.ChangeAction
---@class (exact) oil.CreateAction
---@class (exact) canola.CreateAction
---@field type "create"
---@field url string
---@field entry_type oil.EntryType
---@field entry_type canola.EntryType
---@field link nil|string
---@class (exact) oil.DeleteAction
---@class (exact) canola.DeleteAction
---@field type "delete"
---@field url string
---@field entry_type oil.EntryType
---@field entry_type canola.EntryType
---@class (exact) oil.MoveAction
---@class (exact) canola.MoveAction
---@field type "move"
---@field entry_type oil.EntryType
---@field entry_type canola.EntryType
---@field src_url string
---@field dest_url string
---@class (exact) oil.CopyAction
---@class (exact) canola.CopyAction
---@field type "copy"
---@field entry_type oil.EntryType
---@field entry_type canola.EntryType
---@field src_url string
---@field dest_url string
---@class (exact) oil.ChangeAction
---@class (exact) canola.ChangeAction
---@field type "change"
---@field entry_type oil.EntryType
---@field entry_type canola.EntryType
---@field url string
---@field column string
---@field value any
---@param all_diffs table<integer, oil.Diff[]>
---@return oil.Action[]
---@param all_diffs table<integer, canola.Diff[]>
---@return canola.Action[]
M.create_actions_from_diffs = function(all_diffs)
---@type oil.Action[]
---@type canola.Action[]
local actions = {}
---@type table<integer, oil.Diff[]>
---@type table<integer, canola.Diff[]>
local diff_by_id = setmetatable({}, {
__index = function(t, key)
local list = {}
@ -69,11 +69,11 @@ M.create_actions_from_diffs = function(all_diffs)
-- > foo/bar/b.txt
local seen_creates = {}
---@param action oil.Action
---@param action canola.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 action.type == 'create' then
if seen_creates[action.url] then
return
else
@ -87,11 +87,11 @@ M.create_actions_from_diffs = function(all_diffs)
for bufnr, diffs in pairs(all_diffs) do
local adapter = util.get_adapter(bufnr, true)
if not adapter then
error("Missing adapter")
error('Missing adapter')
end
local parent_url = vim.api.nvim_buf_get_name(bufnr)
for _, diff in ipairs(diffs) do
if diff.type == "new" then
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
@ -100,28 +100,28 @@ M.create_actions_from_diffs = function(all_diffs)
table.insert(by_id, diff)
else
-- Parse nested files like foo/bar/baz
local path_sep = fs.is_windows and "[/\\]" or "/"
local path_sep = fs.is_windows and '[/\\]' or '/'
local pieces = vim.split(diff.name, path_sep)
local url = parent_url:gsub("/$", "")
local url = parent_url:gsub('/$', '')
for i, v in ipairs(pieces) do
local is_last = i == #pieces
local entry_type = is_last and diff.entry_type or "directory"
local alternation = v:match("{([^}]+)}")
local entry_type = is_last and diff.entry_type or 'directory'
local alternation = v:match('{([^}]+)}')
if is_last and alternation then
-- Parse alternations like foo.{js,test.js}
for _, alt in ipairs(vim.split(alternation, ",")) do
local alt_url = url .. "/" .. v:gsub("{[^}]+}", alt)
for _, alt in ipairs(vim.split(alternation, ',')) do
local alt_url = url .. '/' .. v:gsub('{[^}]+}', alt)
add_action({
type = "create",
type = 'create',
url = alt_url,
entry_type = entry_type,
link = diff.link,
})
end
else
url = url .. "/" .. v
url = url .. '/' .. v
add_action({
type = "create",
type = 'create',
url = url,
entry_type = entry_type,
link = diff.link,
@ -129,9 +129,9 @@ M.create_actions_from_diffs = function(all_diffs)
end
end
end
elseif diff.type == "change" then
elseif diff.type == 'change' then
add_action({
type = "change",
type = 'change',
url = parent_url .. diff.name,
entry_type = diff.entry_type,
column = diff.column,
@ -151,7 +151,7 @@ M.create_actions_from_diffs = function(all_diffs)
for id, diffs in pairs(diff_by_id) do
local entry = cache.get_entry_by_id(id)
if not entry then
error(string.format("Could not find entry %d", id))
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
@ -161,7 +161,7 @@ M.create_actions_from_diffs = function(all_diffs)
-- MOVE (+ optional copies) when has both creates and delete
for i, diff in ipairs(diffs) do
add_action({
type = i == #diffs and "move" or "copy",
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
@ -172,7 +172,7 @@ M.create_actions_from_diffs = function(all_diffs)
else
-- DELETE when no create
add_action({
type = "delete",
type = 'delete',
entry_type = entry[FIELD_TYPE],
url = cache.get_parent_url(id) .. entry[FIELD_NAME],
})
@ -181,7 +181,7 @@ M.create_actions_from_diffs = function(all_diffs)
-- COPY when create but no delete
for _, diff in ipairs(diffs) do
add_action({
type = "copy",
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
@ -195,15 +195,15 @@ M.create_actions_from_diffs = function(all_diffs)
return M.enforce_action_order(actions)
end
---@param actions oil.Action[]
---@return oil.Action[]
---@param actions canola.Action[]
---@return canola.Action[]
M.enforce_action_order = function(actions)
local src_trie = Trie.new()
local dest_trie = Trie.new()
for _, action in ipairs(actions) do
if action.type == "delete" or action.type == "change" then
if action.type == 'delete' or action.type == 'change' then
src_trie:insert_action(action.url, action)
elseif action.type == "create" then
elseif action.type == 'create' then
dest_trie:insert_action(action.url, action)
else
dest_trie:insert_action(action.dest_url, action)
@ -220,21 +220,21 @@ M.enforce_action_order = function(actions)
---Gets the dependencies of a particular action. Effectively dynamically calculates the dependency
---"edges" of the graph.
---@param action oil.Action
---@param action canola.Action
local function get_deps(action)
local ret = {}
if action.type == "delete" then
if action.type == 'delete' then
src_trie:accum_children_of(action.url, ret)
elseif action.type == "create" then
elseif action.type == 'create' then
-- Finish operating on parents first
-- e.g. NEW /a BEFORE NEW /a/b
dest_trie:accum_first_parents_of(action.url, ret)
-- Process remove path before creating new path
-- e.g. DELETE /a BEFORE NEW /a
src_trie:accum_actions_at(action.url, ret, function(a)
return a.type == "move" or a.type == "delete"
return a.type == 'move' or a.type == 'delete'
end)
elseif action.type == "change" then
elseif action.type == 'change' then
-- Finish operating on parents first
-- e.g. NEW /a BEFORE CHANGE /a/b
dest_trie:accum_first_parents_of(action.url, ret)
@ -244,9 +244,9 @@ M.enforce_action_order = function(actions)
-- Finish copy from operations first
-- e.g. COPY /a -> /b BEFORE CHANGE /a
src_trie:accum_actions_at(action.url, ret, function(entry)
return entry.type == "copy"
return entry.type == 'copy'
end)
elseif action.type == "move" then
elseif action.type == 'move' then
-- Finish operating on parents first
-- e.g. NEW /a BEFORE MOVE /z -> /a/b
dest_trie:accum_first_parents_of(action.dest_url, ret)
@ -260,9 +260,9 @@ M.enforce_action_order = function(actions)
-- Process remove path before moving to new path
-- e.g. MOVE /a -> /b BEFORE MOVE /c -> /a
src_trie:accum_actions_at(action.dest_url, ret, function(a)
return a.type == "move" or a.type == "delete"
return a.type == 'move' or a.type == 'delete'
end)
elseif action.type == "copy" then
elseif action.type == 'copy' then
-- Finish operating on parents first
-- e.g. NEW /a BEFORE COPY /z -> /a/b
dest_trie:accum_first_parents_of(action.dest_url, ret)
@ -272,14 +272,14 @@ M.enforce_action_order = function(actions)
-- Process remove path before copying to new path
-- e.g. MOVE /a -> /b BEFORE COPY /c -> /a
src_trie:accum_actions_at(action.dest_url, ret, function(a)
return a.type == "move" or a.type == "delete"
return a.type == 'move' or a.type == 'delete'
end)
end
return ret
end
---@return nil|oil.Action The leaf action
---@return nil|oil.Action When no leaves found, this is the last action in the loop
---@return nil|canola.Action The leaf action
---@return nil|canola.Action When no leaves found, this is the last action in the loop
local function find_leaf(action, seen)
if not seen then
seen = {}
@ -312,24 +312,24 @@ M.enforce_action_order = function(actions)
if selected then
to_remove = selected
else
if loop_action and loop_action.type == "move" then
if loop_action and loop_action.type == 'move' then
-- If this is moving a parent into itself, that's an error
if vim.startswith(loop_action.dest_url, loop_action.src_url) then
error("Detected cycle in desired paths")
error('Detected cycle in desired paths')
end
-- We've detected a move cycle (e.g. MOVE /a -> /b + MOVE /b -> /a)
-- Split one of the moves and retry
local intermediate_url =
string.format("%s__oil_tmp_%05d", loop_action.src_url, math.random(999999))
string.format('%s__canola_tmp_%05d', loop_action.src_url, math.random(999999))
local move_1 = {
type = "move",
type = 'move',
entry_type = loop_action.entry_type,
src_url = loop_action.src_url,
dest_url = intermediate_url,
}
local move_2 = {
type = "move",
type = 'move',
entry_type = loop_action.entry_type,
src_url = intermediate_url,
dest_url = loop_action.dest_url,
@ -340,16 +340,16 @@ M.enforce_action_order = function(actions)
dest_trie:insert_action(move_1.dest_url, move_1)
src_trie:insert_action(move_1.src_url, move_1)
else
error("Detected cycle in desired paths")
error('Detected cycle in desired paths')
end
end
if selected then
if selected.type == "move" or selected.type == "copy" then
if vim.startswith(selected.dest_url, selected.src_url .. "/") then
if selected.type == 'move' or selected.type == 'copy' then
if vim.startswith(selected.dest_url, selected.src_url .. '/') then
error(
string.format(
"Cannot move or copy parent into itself: %s -> %s",
'Cannot move or copy parent into itself: %s -> %s',
selected.src_url,
selected.dest_url
)
@ -360,9 +360,9 @@ M.enforce_action_order = function(actions)
end
if to_remove then
if to_remove.type == "delete" or to_remove.type == "change" then
if to_remove.type == 'delete' or to_remove.type == 'change' then
src_trie:remove_action(to_remove.url, to_remove)
elseif to_remove.type == "create" then
elseif to_remove.type == 'create' then
dest_trie:remove_action(to_remove.url, to_remove)
else
dest_trie:remove_action(to_remove.dest_url, to_remove)
@ -383,12 +383,12 @@ end
local progress
---@param actions oil.Action[]
---@param actions canola.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 } }
'User',
{ pattern = 'CanolaActionsPre', modeline = false, data = { actions = actions } }
)
local did_complete = nil
@ -398,14 +398,14 @@ M.process_actions = function(actions, cb)
-- Convert some cross-adapter moves to a copy + delete
for _, action in ipairs(actions) do
if action.type == "move" then
if action.type == 'move' then
local _, cross_action = util.get_adapter_for_action(action)
-- Only do the conversion if the cross-adapter support is "copy"
if cross_action == "copy" then
if cross_action == 'copy' then
---@diagnostic disable-next-line: assign-type-mismatch
action.type = "copy"
action.type = 'copy'
table.insert(actions, {
type = "delete",
type = 'delete',
url = action.src_url,
entry_type = action.entry_type,
})
@ -420,9 +420,24 @@ M.process_actions = function(actions, cb)
finished = true
progress:close()
progress = nil
if config.cleanup_buffers_on_delete and not err then
for _, action in ipairs(actions) do
if action.type == 'delete' then
local scheme, path = util.parse_url(action.url)
if config.adapters[scheme] == 'files' then
assert(path)
local os_path = fs.posix_to_os_path(path)
local bufnr = vim.fn.bufnr(os_path)
if bufnr ~= -1 then
vim.api.nvim_buf_delete(bufnr, { force = true })
end
end
end
end
end
vim.api.nvim_exec_autocmds(
"User",
{ pattern = "OilActionsPost", modeline = false, data = { err = err, actions = actions } }
'User',
{ pattern = 'CanolaActionsPost', modeline = false, data = { err = err, actions = actions } }
)
cb(err)
end
@ -435,7 +450,7 @@ M.process_actions = function(actions, cb)
-- TODO some actions are actually cancelable.
-- We should stop them instead of stopping after the current action
cancel = function()
finish("Canceled")
finish('Canceled')
end,
})
end
@ -472,8 +487,8 @@ M.process_actions = function(actions, cb)
next_action()
end
end)
if action.type == "change" then
---@cast action oil.ChangeAction
if action.type == 'change' then
---@cast action canola.ChangeAction
columns.perform_change_action(adapter, action, callback)
else
adapter.perform_action(action, callback)
@ -502,14 +517,14 @@ M.try_write_changes = function(confirm, cb)
cb = function(_err) end
end
if mutation_in_progress then
cb("Cannot perform mutation when already in progress")
cb('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<integer, oil.ParseError[]>
---@type table<integer, canola.ParseError[]>
local all_errors = {}
mutation_in_progress = true
@ -537,7 +552,7 @@ M.try_write_changes = function(confirm, cb)
mutation_in_progress = false
end
local ns = vim.api.nvim_create_namespace("Oil")
local ns = vim.api.nvim_create_namespace('Canola')
vim.diagnostic.reset(ns)
if not vim.tbl_isempty(all_errors) then
for bufnr, errors in pairs(all_errors) do
@ -564,7 +579,7 @@ M.try_write_changes = function(confirm, cb)
end)
end
unlock()
cb("Error parsing oil buffers")
cb('Error parsing canola buffers')
return
end
@ -572,7 +587,7 @@ M.try_write_changes = function(confirm, cb)
confirmation.show(actions, confirm, function(proceed)
if not proceed then
unlock()
cb("Canceled")
cb('Canceled')
return
end
@ -581,23 +596,23 @@ 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()
err = string.format('[canola] Error applying actions: %s', err)
view.rerender_all_canola_buffers(nil, function()
cb(err)
end)
else
local current_entry = oil.get_cursor_entry()
local current_entry = canola.get_cursor_entry()
if current_entry then
-- get the entry under the cursor and make sure the cursor stays on it
view.set_last_cursor(
vim.api.nvim_buf_get_name(0),
vim.split(current_entry.parsed_name or current_entry.name, "/")[1]
vim.split(current_entry.parsed_name or current_entry.name, '/')[1]
)
end
view.rerender_all_oil_buffers(nil, function(render_err)
view.rerender_all_canola_buffers(nil, function(render_err)
vim.api.nvim_exec_autocmds(
"User",
{ pattern = "OilMutationComplete", modeline = false }
'User',
{ pattern = 'CanolaMutationComplete', modeline = false }
)
cb(render_err)
end)

View file

@ -1,10 +1,10 @@
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")
local view = require("oil.view")
local cache = require('canola.cache')
local columns = require('canola.columns')
local config = require('canola.config')
local constants = require('canola.constants')
local fs = require('canola.fs')
local util = require('canola.util')
local view = require('canola.view')
local M = {}
local FIELD_ID = constants.FIELD_ID
@ -12,23 +12,23 @@ local FIELD_NAME = constants.FIELD_NAME
local FIELD_TYPE = constants.FIELD_TYPE
local FIELD_META = constants.FIELD_META
---@alias oil.Diff oil.DiffNew|oil.DiffDelete|oil.DiffChange
---@alias canola.Diff canola.DiffNew|canola.DiffDelete|canola.DiffChange
---@class (exact) oil.DiffNew
---@class (exact) canola.DiffNew
---@field type "new"
---@field name string
---@field entry_type oil.EntryType
---@field entry_type canola.EntryType
---@field id nil|integer
---@field link nil|string
---@class (exact) oil.DiffDelete
---@class (exact) canola.DiffDelete
---@field type "delete"
---@field name string
---@field id integer
---@class (exact) oil.DiffChange
---@class (exact) canola.DiffChange
---@field type "change"
---@field entry_type oil.EntryType
---@field entry_type canola.EntryType
---@field name string
---@field column string
---@field value any
@ -37,7 +37,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, '/') or (fs.is_windows and vim.endswith(name, '\\'))
if isdir then
name = name:sub(1, name:len() - 1)
end
@ -52,29 +52,29 @@ local function compare_link_target(meta, parsed_entry)
return false
end
-- Make sure we trim off any trailing path slashes from both sources
local meta_name = meta.link:gsub("[/\\]$", "")
local parsed_name = parsed_entry.link_target:gsub("[/\\]$", "")
local meta_name = meta.link:gsub('[/\\]$', '')
local parsed_name = parsed_entry.link_target:gsub('[/\\]$', '')
return meta_name == parsed_name
end
---@class (exact) oil.ParseResult
---@class (exact) canola.ParseResult
---@field data table Parsed entry data
---@field ranges table<string, integer[]> Locations of the various columns
---@field entry nil|oil.InternalEntry If the entry already exists
---@field entry nil|canola.InternalEntry If the entry already exists
---Parse a single line in a buffer
---@param adapter oil.Adapter
---@param adapter canola.Adapter
---@param line string
---@param column_defs oil.ColumnSpec[]
---@return nil|oil.ParseResult
---@param column_defs canola.ColumnSpec[]
---@return nil|canola.ParseResult
---@return nil|string Error
M.parse_line = function(adapter, line, column_defs)
local ret = {}
local ranges = {}
local start = 1
local value, rem = line:match("^/(%d+) (.+)$")
local value, rem = line:match('^/(%d+) (.+)$')
if not value then
return nil, "Malformed ID at start of line"
return nil, 'Malformed ID at start of line'
end
ranges.id = { start, value:len() + 1 }
start = ranges.id[2] + 1
@ -97,7 +97,7 @@ M.parse_line = function(adapter, line, column_defs)
local start_len = string.len(rem)
value, rem = columns.parse_col(adapter, assert(rem), def)
if not rem then
return nil, string.format("Parsing %s failed", name)
return nil, string.format('Parsing %s failed', name)
end
ret[name] = value
range[2] = range[1] + start_len - string.len(rem) - 1
@ -108,10 +108,10 @@ M.parse_line = function(adapter, line, column_defs)
if name then
local isdir
name, isdir = parsedir(vim.trim(name))
if name ~= "" then
if name ~= '' then
ret.name = name
end
ret._type = isdir and "directory" or "file"
ret._type = isdir and 'directory' or 'file'
end
local entry = cache.get_entry_by_id(ret.id)
ranges.name = { start, start + string.len(rem) - 1 }
@ -122,38 +122,38 @@ M.parse_line = function(adapter, line, column_defs)
-- Parse the symlink syntax
local meta = entry[FIELD_META]
local entry_type = entry[FIELD_TYPE]
if entry_type == "link" and meta and meta.link then
local name_pieces = vim.split(ret.name, " -> ", { plain = true })
if entry_type == 'link' and meta and meta.link then
local name_pieces = vim.split(ret.name, ' -> ', { plain = true })
if #name_pieces ~= 2 then
ret.name = ""
ret.name = ''
return { data = ret, ranges = ranges }
end
ranges.name = { start, start + string.len(name_pieces[1]) - 1 }
ret.name = parsedir(vim.trim(name_pieces[1]))
ret.link_target = name_pieces[2]
ret._type = "link"
ret._type = 'link'
end
-- Try to keep the same file type
if entry_type ~= "directory" and entry_type ~= "file" and ret._type ~= "directory" then
if entry_type ~= 'directory' and entry_type ~= 'file' and ret._type ~= 'directory' then
ret._type = entry[FIELD_TYPE]
end
return { data = ret, entry = entry, ranges = ranges }
end
---@class (exact) oil.ParseError
---@class (exact) canola.ParseError
---@field lnum integer
---@field col integer
---@field message string
---@param bufnr integer
---@return oil.Diff[] diffs
---@return oil.ParseError[] errors Parsing errors
---@return canola.Diff[] diffs
---@return canola.ParseError[] errors Parsing errors
M.parse = function(bufnr)
---@type oil.Diff[]
---@type canola.Diff[]
local diffs = {}
---@type oil.ParseError[]
---@type canola.ParseError[]
local errors = {}
local bufname = vim.api.nvim_buf_get_name(bufnr)
local adapter = util.get_adapter(bufnr, true)
@ -187,7 +187,7 @@ 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, end_lnum = i, col = 0 })
else
seen_names[name] = true
end
@ -197,7 +197,7 @@ M.parse = function(bufnr)
-- hack to be compatible with Lua 5.1
-- use return instead of goto
(function()
if line:match("^/%d+") then
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
@ -217,11 +217,11 @@ M.parse = function(bufnr)
local err_message
if not parsed_entry.name then
err_message = "No filename found"
err_message = 'No filename found'
elseif not entry then
err_message = "Could not find existing entry (was the ID changed?)"
elseif parsed_entry.name:match("/") or parsed_entry.name:match(fs.sep) then
err_message = "Filename cannot contain path separator"
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, {
@ -237,16 +237,16 @@ M.parse = function(bufnr)
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
if entry[FIELD_TYPE] == 'link' and not compare_link_target(meta, parsed_entry) then
table.insert(diffs, {
type = "new",
type = 'new',
name = parsed_entry.name,
entry_type = "link",
entry_type = 'link',
link = parsed_entry.link_target,
})
elseif entry[FIELD_TYPE] ~= parsed_entry._type then
table.insert(diffs, {
type = "new",
type = 'new',
name = parsed_entry.name,
entry_type = parsed_entry._type,
})
@ -255,7 +255,7 @@ M.parse = function(bufnr)
end
else
table.insert(diffs, {
type = "new",
type = 'new',
name = parsed_entry.name,
entry_type = parsed_entry._type,
id = parsed_entry.id,
@ -267,7 +267,7 @@ M.parse = function(bufnr)
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",
type = 'change',
name = parsed_entry.name,
entry_type = entry[FIELD_TYPE],
column = col_name,
@ -278,7 +278,7 @@ M.parse = function(bufnr)
else
-- Parse a new entry
local name, isdir = parsedir(vim.trim(line))
if vim.startswith(name, "/") then
if vim.startswith(name, '/') then
table.insert(errors, {
message = "Paths cannot start with '/'",
lnum = i - 1,
@ -287,17 +287,17 @@ M.parse = function(bufnr)
})
return
end
if name ~= "" then
local link_pieces = vim.split(name, " -> ", { plain = true })
local entry_type = isdir and "directory" or "file"
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"
entry_type = 'link'
name, link = unpack(link_pieces)
end
check_dupe(name, i)
table.insert(diffs, {
type = "new",
type = 'new',
name = name,
entry_type = entry_type,
link = link,
@ -309,7 +309,7 @@ M.parse = function(bufnr)
for name, child_id in pairs(original_entries) do
table.insert(diffs, {
type = "delete",
type = 'delete',
name = name,
id = child_id,
})

View file

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

View file

@ -1,13 +1,13 @@
local util = require("oil.util")
local util = require('canola.util')
---@class (exact) oil.Trie
---@field new fun(): oil.Trie
---@class (exact) canola.Trie
---@field new fun(): canola.Trie
---@field private root table
local Trie = {}
---@return oil.Trie
---@return canola.Trie
Trie.new = function()
---@type oil.Trie
---@type canola.Trie
return setmetatable({
root = { values = {}, children = {} },
}, {
@ -20,7 +20,7 @@ end
function Trie:_url_to_path_pieces(url)
local scheme, path = util.parse_url(url)
assert(path)
local pieces = vim.split(path, "/")
local pieces = vim.split(path, '/')
table.insert(pieces, 1, scheme)
return pieces
end
@ -75,12 +75,12 @@ function Trie:remove(path_pieces, value)
return
end
end
error("Value not present in trie: " .. vim.inspect(value))
error('Value not present in trie: ' .. vim.inspect(value))
end
---Add the first action that affects a parent path of the url
---@param url string
---@param ret oil.InternalEntry[]
---@param ret canola.InternalEntry[]
function Trie:accum_first_parents_of(url, ret)
local pieces = self:_url_to_path_pieces(url)
local containers = { self.root }
@ -117,8 +117,8 @@ 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 ret canola.InternalEntry[]
---@param filter nil|fun(entry: canola.Action): boolean
function Trie:accum_children_of(url, ret, filter)
local pieces = self:_url_to_path_pieces(url)
local current = self.root
@ -137,8 +137,8 @@ end
---Add all actions at a specific path
---@param url string
---@param ret oil.InternalEntry[]
---@param filter? fun(entry: oil.Action): boolean
---@param ret canola.InternalEntry[]
---@param filter? fun(entry: canola.Action): boolean
function Trie:accum_actions_at(url, ret, filter)
local pieces = self:_url_to_path_pieces(url)
local current = self.root

29
lua/canola/pathutil.lua Normal file
View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
local config = require("oil.config")
local constants = require("oil.constants")
local config = require('canola.config')
local constants = require('canola.constants')
local M = {}
@ -8,13 +8,13 @@ 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?, ft: string?): (icon: string, hl: string)
---@alias canola.IconProvider fun(type: string, name: string, conf: table?, ft: string?): (icon: string, hl: string)
---@param url string
---@return nil|string
---@return nil|string
M.parse_url = function(url)
return url:match("^(.*://)(.*)$")
return url:match('^(.*://)(.*)$')
end
---Escapes a filename for use in :edit
@ -26,64 +26,64 @@ M.escape_filename = function(filename)
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"] = "~",
['20'] = ' ',
['22'] = '',
['23'] = '#',
['24'] = '$',
['25'] = '%',
['26'] = '&',
['27'] = '',
['2B'] = '+',
['2C'] = ',',
['2F'] = '/',
['3A'] = ':',
['3B'] = ';',
['3C'] = '<',
['3D'] = '=',
['3E'] = '>',
['3F'] = '?',
['40'] = '@',
['5B'] = '[',
['5C'] = '\\',
['5D'] = ']',
['5E'] = '^',
['60'] = '`',
['7B'] = '{',
['7C'] = '|',
['7D'] = '}',
['7E'] = '~',
}
local _char_to_url_escape = {}
for k, v in pairs(_url_escape_to_char) do
_char_to_url_escape[v] = "%" .. k
_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))
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)
string:gsub('%%([0-9A-Fa-f][0-9A-Fa-f])', function(seq)
return _url_escape_to_char[seq:upper()] or ('%' .. seq)
end)
)
end
---@param bufnr integer
---@param silent? boolean
---@return nil|oil.Adapter
---@return nil|canola.Adapter
M.get_adapter = function(bufnr, silent)
local bufname = vim.api.nvim_buf_get_name(bufnr)
local adapter = config.get_adapter_by_scheme(bufname)
if not adapter and not silent then
vim.notify_once(
string.format("[oil] could not find adapter for buffer '%s://'", bufname),
string.format("[canola] could not find adapter for buffer '%s://'", bufname),
vim.log.levels.ERROR
)
end
@ -92,7 +92,7 @@ end
---@param text string
---@param width integer|nil
---@param align oil.ColumnAlign
---@param align canola.ColumnAlign
---@return string padded_text
---@return integer left_padding
M.pad_align = function(text, width, align)
@ -105,14 +105,14 @@ M.pad_align = function(text, width, align)
return text, 0
end
if align == "right" then
return string.rep(" ", total_pad) .. text, total_pad
elseif align == "center" then
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
return string.rep(' ', left_pad) .. text .. string.rep(' ', right_pad), left_pad
else
return text .. string.rep(" ", total_pad), 0
return text .. string.rep(' ', total_pad), 0
end
end
@ -135,8 +135,8 @@ M.tbl_slice = function(tbl, start_idx, end_idx)
return ret
end
---@param entry oil.InternalEntry
---@return oil.Entry
---@param entry canola.InternalEntry
---@return canola.Entry
M.export_entry = function(entry)
return {
name = entry[FIELD_NAME],
@ -150,7 +150,7 @@ end
---@param dest_buf_name string
---@return boolean True if the buffer was replaced instead of renamed
M.rename_buffer = function(src_bufnr, dest_buf_name)
if type(src_bufnr) == "string" then
if type(src_bufnr) == 'string' then
src_bufnr = vim.fn.bufadd(src_bufnr)
if not vim.api.nvim_buf_is_loaded(src_bufnr) then
vim.api.nvim_buf_delete(src_bufnr, {})
@ -164,7 +164,7 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
-- think that the new buffer conflicts with the file next time it tries to save.
if not vim.loop.fs_stat(dest_buf_name) then
---@diagnostic disable-next-line: param-type-mismatch
local altbuf = vim.fn.bufnr("#")
local altbuf = vim.fn.bufnr('#')
-- This will fail if the dest buf name already exists
local ok = pcall(vim.api.nvim_buf_set_name, src_bufnr, dest_buf_name)
if ok then
@ -173,7 +173,7 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
-- where Neovim doesn't allow buffer modifications.
pcall(vim.api.nvim_buf_delete, vim.fn.bufadd(bufname), {})
if altbuf and vim.api.nvim_buf_is_valid(altbuf) then
vim.fn.setreg("#", altbuf)
vim.fn.setreg('#', altbuf)
end
return false
@ -229,6 +229,7 @@ end
M.cb_collect = function(count, cb)
return function(err)
if err then
-- selene: allow(mismatched_arg_count)
cb(err)
cb = function() end
else
@ -243,22 +244,22 @@ end
---@param url string
---@return string[]
local function get_possible_buffer_names_from_url(url)
local fs = require("oil.fs")
local fs = require('canola.fs')
local scheme, path = M.parse_url(url)
if config.adapters[scheme] == "files" then
if config.adapters[scheme] == 'files' then
assert(path)
return { fs.posix_to_os_path(path) }
end
return { url }
end
---@param entry_type oil.EntryType
---@param entry_type canola.EntryType
---@param src_url string
---@param dest_url string
M.update_moved_buffers = function(entry_type, src_url, dest_url)
local src_buf_names = get_possible_buffer_names_from_url(src_url)
local dest_buf_name = get_possible_buffer_names_from_url(dest_url)[1]
if entry_type ~= "directory" then
if entry_type ~= 'directory' then
for _, src_buf_name in ipairs(src_buf_names) do
M.rename_buffer(src_buf_name, dest_buf_name)
end
@ -270,15 +271,15 @@ M.update_moved_buffers = function(entry_type, src_url, dest_url)
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
local bufname = vim.api.nvim_buf_get_name(bufnr)
if vim.startswith(bufname, src_url) then
-- Handle oil directory buffers
-- Handle canola directory buffers
vim.api.nvim_buf_set_name(bufnr, dest_url .. bufname:sub(src_url:len() + 1))
elseif bufname ~= "" and vim.bo[bufnr].buftype == "" then
elseif bufname ~= '' and vim.bo[bufnr].buftype == '' then
-- Handle regular buffers
local scheme = M.parse_url(bufname)
-- If the buffer is a local file, make sure we're using the absolute path
if not scheme then
bufname = vim.fn.fnamemodify(bufname, ":p")
bufname = vim.fn.fnamemodify(bufname, ':p')
end
for _, src_buf_name in ipairs(src_buf_names) do
@ -296,23 +297,23 @@ end
---@return string
---@return table|nil
M.split_config = function(name_or_config)
if type(name_or_config) == "string" then
if type(name_or_config) == 'string' then
return name_or_config, nil
else
if not name_or_config[1] and name_or_config["1"] then
if not name_or_config[1] and name_or_config['1'] then
-- This was likely loaded from json, so the first element got coerced to a string key
name_or_config[1] = name_or_config["1"]
name_or_config["1"] = nil
name_or_config[1] = name_or_config['1']
name_or_config['1'] = nil
end
return name_or_config[1], name_or_config
end
end
---@alias oil.ColumnAlign "left"|"center"|"right"
---@alias canola.ColumnAlign "left"|"center"|"right"
---@param lines oil.TextChunk[][]
---@param lines canola.TextChunk[][]
---@param col_width integer[]
---@param col_align? oil.ColumnAlign[]
---@param col_align? canola.ColumnAlign[]
---@return string[]
---@return any[][] List of highlights {group, lnum, col_start, col_end}
M.render_table = function(lines, col_width, col_align)
@ -324,7 +325,7 @@ M.render_table = function(lines, col_width, col_align)
local pieces = {}
for i, chunk in ipairs(cols) do
local text, hl
if type(chunk) == "table" then
if type(chunk) == 'table' then
text = chunk[1]
hl = chunk[2]
else
@ -333,11 +334,11 @@ M.render_table = function(lines, col_width, col_align)
local unpadded_len = text:len()
local padding
text, padding = M.pad_align(text, col_width[i], col_align[i] or "left")
text, padding = M.pad_align(text, col_width[i], col_align[i] or 'left')
table.insert(pieces, text)
if hl then
if type(hl) == "table" then
if type(hl) == 'table' then
-- hl has the form { [1]: hl_name, [2]: col_start, [3]: col_end }[]
-- Notice that col_start and col_end are relative position inside
-- that col, so we need to add the offset to them
@ -355,7 +356,7 @@ M.render_table = function(lines, col_width, col_align)
end
col = col + text:len() + 1
end
table.insert(str_lines, table.concat(pieces, " "))
table.insert(str_lines, table.concat(pieces, ' '))
end
return str_lines, highlights
end
@ -363,7 +364,7 @@ end
---@param bufnr integer
---@param highlights any[][] List of highlights {group, lnum, col_start, col_end}
M.set_highlights = function(bufnr, highlights)
local ns = vim.api.nvim_create_namespace("Oil")
local ns = vim.api.nvim_create_namespace('Canola')
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)
@ -379,12 +380,12 @@ end
---@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 = "\\"
local slash = '/'
if os_slash and require('canola.fs').is_windows then
slash = '\\'
end
local endslash = path:match(slash .. "$")
local endslash = path:match(slash .. '$')
if not endslash then
return path .. slash
else
@ -395,7 +396,7 @@ end
---@param winid nil|integer
---@return boolean
M.is_floating_win = function(winid)
return vim.api.nvim_win_get_config(winid or 0).relative ~= ""
return vim.api.nvim_win_get_config(winid or 0).relative ~= ''
end
---Recalculate the window title for the current buffer
@ -410,10 +411,10 @@ M.get_title = function(winid)
local title = vim.api.nvim_buf_get_name(src_buf)
local scheme, path = M.parse_url(title)
if config.adapters[scheme] == "files" then
if config.adapters[scheme] == 'files' then
assert(path)
local fs = require("oil.fs")
title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~")
local fs = require('canola.fs')
title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ':~')
end
return title
end
@ -421,7 +422,7 @@ end
local winid_map = {}
M.add_title_to_win = function(winid, opts)
opts = opts or {}
opts.align = opts.align or "left"
opts.align = opts.align or 'left'
if not vim.api.nvim_win_is_valid(winid) then
return
end
@ -438,18 +439,18 @@ M.add_title_to_win = function(winid, opts)
else
bufnr = vim.api.nvim_create_buf(false, true)
local col = 1
if opts.align == "center" then
if opts.align == 'center' then
col = math.floor((vim.api.nvim_win_get_width(winid) - width) / 2)
elseif opts.align == "right" then
elseif opts.align == 'right' then
col = vim.api.nvim_win_get_width(winid) - 1 - width
elseif opts.align ~= "left" then
elseif opts.align ~= 'left' then
vim.notify(
string.format("Unknown oil window title alignment: '%s'", opts.align),
string.format("Unknown canola window title alignment: '%s'", opts.align),
vim.log.levels.ERROR
)
end
title_winid = vim.api.nvim_open_win(bufnr, false, {
relative = "win",
relative = 'win',
win = winid,
width = width,
height = 1,
@ -457,20 +458,20 @@ M.add_title_to_win = function(winid, opts)
col = col,
focusable = false,
zindex = 151,
style = "minimal",
style = 'minimal',
noautocmd = true,
})
winid_map[winid] = title_winid
vim.api.nvim_set_option_value(
"winblend",
'winblend',
vim.wo[winid].winblend,
{ scope = "local", win = title_winid }
{ scope = 'local', win = title_winid }
)
vim.bo[bufnr].bufhidden = "wipe"
vim.bo[bufnr].bufhidden = 'wipe'
local update_autocmd = vim.api.nvim_create_autocmd("BufWinEnter", {
desc = "Update oil floating window title when buffer changes",
pattern = "*",
local update_autocmd = vim.api.nvim_create_autocmd('BufWinEnter', {
desc = 'Update canola floating window title when buffer changes',
pattern = '*',
callback = function(params)
local winbuf = params.buf
if vim.api.nvim_win_get_buf(winid) ~= winbuf then
@ -479,17 +480,17 @@ M.add_title_to_win = function(winid, opts)
local new_title = M.get_title(winid)
local new_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 .. " " })
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { ' ' .. new_title .. ' ' })
vim.bo[bufnr].modified = false
vim.api.nvim_win_set_width(title_winid, new_width)
local new_col = 1
if opts.align == "center" then
if opts.align == 'center' then
new_col = math.floor((vim.api.nvim_win_get_width(winid) - new_width) / 2)
elseif opts.align == "right" then
elseif opts.align == 'right' then
new_col = vim.api.nvim_win_get_width(winid) - 1 - new_width
end
vim.api.nvim_win_set_config(title_winid, {
relative = "win",
relative = 'win',
win = winid,
row = -1,
col = new_col,
@ -498,8 +499,8 @@ M.add_title_to_win = function(winid, opts)
})
end,
})
vim.api.nvim_create_autocmd("WinClosed", {
desc = "Close oil floating window title when floating window closes",
vim.api.nvim_create_autocmd('WinClosed', {
desc = 'Close canola floating window title when floating window closes',
pattern = tostring(winid),
callback = function()
if title_winid and vim.api.nvim_win_is_valid(title_winid) then
@ -512,18 +513,18 @@ M.add_title_to_win = function(winid, opts)
nested = true,
})
end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { " " .. title .. " " })
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { ' ' .. title .. ' ' })
vim.bo[bufnr].modified = false
vim.api.nvim_set_option_value(
"winhighlight",
"Normal:FloatTitle,NormalFloat:FloatTitle",
{ scope = "local", win = title_winid }
'winhighlight',
'Normal:FloatTitle,NormalFloat:FloatTitle',
{ scope = 'local', win = title_winid }
)
end
---@param action oil.Action
---@return oil.Adapter
---@return nil|oil.CrossAdapterAction
---@param action canola.Action
---@return canola.Adapter
---@return nil|canola.CrossAdapterAction
M.get_adapter_for_action = function(action)
local adapter = assert(config.get_adapter_by_scheme(action.url or action.src_url))
if action.dest_url then
@ -542,7 +543,7 @@ M.get_adapter_for_action = function(action)
else
error(
string.format(
"Cannot copy files from %s -> %s; no cross-adapter transfer method found",
'Cannot copy files from %s -> %s; no cross-adapter transfer method found',
action.src_url,
action.dest_url
)
@ -559,12 +560,12 @@ end
---@return string
---@return integer
M.h_align = function(str, align, width)
if align == "center" then
if align == 'center' then
local padding = math.floor((width - vim.api.nvim_strwidth(str)) / 2)
return string.rep(" ", padding) .. str, padding
elseif align == "right" then
return string.rep(' ', padding) .. str, padding
elseif align == 'right' then
local padding = width - vim.api.nvim_strwidth(str)
return string.rep(" ", padding) .. str, padding
return string.rep(' ', padding) .. str, padding
else
return str, 0
end
@ -578,15 +579,15 @@ end
--- actions nil|string[]
--- winid nil|integer
M.render_text = function(bufnr, text, opts)
opts = vim.tbl_deep_extend("keep", opts or {}, {
h_align = "center",
v_align = "center",
opts = vim.tbl_deep_extend('keep', opts or {}, {
h_align = 'center',
v_align = 'center',
})
---@cast opts -nil
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end
if type(text) == "string" then
if type(text) == 'string' then
text = { text }
end
local height = 40
@ -608,17 +609,17 @@ M.render_text = function(bufnr, text, opts)
local lines = {}
-- Add vertical spacing for vertical alignment
if opts.v_align == "center" then
if opts.v_align == 'center' then
for _ = 1, (height / 2) - (#text / 2) do
table.insert(lines, "")
table.insert(lines, '')
end
elseif opts.v_align == "bottom" then
elseif opts.v_align == 'bottom' then
local num_lines = height
if opts.actions then
num_lines = num_lines - 2
end
while #lines + #text < num_lines do
table.insert(lines, "")
table.insert(lines, '')
end
end
@ -632,12 +633,12 @@ M.render_text = function(bufnr, text, opts)
local highlights = {}
if opts.actions then
while #lines < height - 1 do
table.insert(lines, "")
table.insert(lines, '')
end
local last_line, padding = M.h_align(table.concat(opts.actions, " "), "center", width)
local last_line, padding = M.h_align(table.concat(opts.actions, ' '), 'center', width)
local col = padding
for _, action in ipairs(opts.actions) do
table.insert(highlights, { "Special", #lines, col, col + 3 })
table.insert(highlights, { 'Special', #lines, col, col + 3 })
col = padding + action:len() + 4
end
table.insert(lines, last_line)
@ -656,10 +657,10 @@ end
M.run_in_fullscreen_win = function(bufnr, callback)
if not bufnr then
bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].bufhidden = "wipe"
vim.bo[bufnr].bufhidden = 'wipe'
end
local winid = vim.api.nvim_open_win(bufnr, false, {
relative = "editor",
relative = 'editor',
width = vim.o.columns,
height = vim.o.lines,
row = 0,
@ -667,19 +668,19 @@ M.run_in_fullscreen_win = function(bufnr, callback)
noautocmd = true,
})
local winnr = vim.api.nvim_win_get_number(winid)
vim.cmd.wincmd({ count = winnr, args = { "w" }, mods = { noautocmd = true } })
vim.cmd.wincmd({ count = winnr, args = { 'w' }, mods = { noautocmd = true } })
callback()
vim.cmd.close({ count = winnr, mods = { noautocmd = true, emsg_silent = true } })
end
---@param bufnr integer
---@return boolean
M.is_oil_bufnr = function(bufnr)
M.is_canola_bufnr = function(bufnr)
local filetype = vim.bo[bufnr].filetype
if filetype == "oil" then
if filetype == 'canola' then
return true
elseif filetype ~= "" then
-- If the filetype is set and is NOT "oil", then it's not an oil buffer
elseif filetype ~= '' then
-- If the filetype is set and is NOT "canola", then it's not an canola buffer
return false
end
local scheme = M.parse_url(vim.api.nvim_buf_get_name(bufnr))
@ -693,10 +694,10 @@ M.hack_around_termopen_autocmd = function(prev_mode)
vim.defer_fn(function()
local new_mode = vim.api.nvim_get_mode().mode
if new_mode ~= prev_mode then
if string.find(new_mode, "i") == 1 then
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<ESC>", true, true, true), "n", false)
if string.find(prev_mode, "v") == 1 or string.find(prev_mode, "V") == 1 then
vim.cmd.normal({ bang = true, args = { "gv" } })
if string.find(new_mode, 'i') == 1 then
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<ESC>', true, true, true), 'n', false)
if string.find(prev_mode, 'v') == 1 or string.find(prev_mode, 'V') == 1 then
vim.cmd.normal({ bang = true, args = { 'gv' } })
end
end
end
@ -712,7 +713,7 @@ M.get_preview_win = function(opts)
if
vim.api.nvim_win_is_valid(winid)
and vim.wo[winid].previewwindow
and (opts.include_not_owned or vim.w[winid]["oil_preview"])
and (opts.include_not_owned or vim.w[winid]['canola_preview'])
then
return winid
end
@ -721,13 +722,13 @@ end
---@return fun() restore Function that restores the cursor
M.hide_cursor = function()
vim.api.nvim_set_hl(0, "OilPreviewCursor", { nocombine = true, blend = 100 })
vim.api.nvim_set_hl(0, 'CanolaPreviewCursor', { nocombine = true, blend = 100 })
local original_guicursor = vim.go.guicursor
vim.go.guicursor = "a:OilPreviewCursor/OilPreviewCursor"
vim.go.guicursor = 'a:CanolaPreviewCursor/CanolaPreviewCursor'
return function()
-- HACK: see https://github.com/neovim/neovim/issues/21018
vim.go.guicursor = "a:"
vim.go.guicursor = 'a:'
vim.cmd.redrawstatus()
vim.go.guicursor = original_guicursor
end
@ -757,12 +758,12 @@ M.buf_get_win = function(bufnr, preferred_win)
return nil
end
---@param adapter oil.Adapter
---@param adapter canola.Adapter
---@param url string
---@param opts {columns?: string[], no_cache?: boolean}
---@param callback fun(err: nil|string, entries: nil|oil.InternalEntry[])
---@param callback fun(err: nil|string, entries: nil|canola.InternalEntry[])
M.adapter_list_all = function(adapter, url, opts, callback)
local cache = require("oil.cache")
local cache = require('canola.cache')
if not opts.no_cache then
local entries = cache.list_url(url)
if not vim.tbl_isempty(entries) then
@ -786,27 +787,27 @@ M.adapter_list_all = function(adapter, url, opts, callback)
end)
end
---Send files from the current oil directory to quickfix
---Send files from the current canola 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
if type(opts) ~= 'table' then
opts = {}
end
local oil = require("oil")
local dir = oil.get_current_dir()
if type(dir) ~= "string" then
local canola = require('canola')
local dir = canola.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("$") }
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 entry = canola.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,
@ -817,26 +818,26 @@ M.send_to_quickfix = function(opts)
end
end
if #qf_entries == 0 then
vim.notify("[oil] No entries found to send to quickfix", vim.log.levels.WARN)
vim.notify('[canola] 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.api.nvim_exec_autocmds('QuickFixCmdPre', {})
local qf_title = 'canola 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", {})
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
return mode:match('^[vV]') ~= nil
end
---Get the current visual selection range. If not in visual mode, return nil.
@ -847,7 +848,7 @@ M.get_visual_range = function()
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 _, 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
@ -855,7 +856,7 @@ M.get_visual_range = function()
return { start_lnum = start_lnum, end_lnum = end_lnum }
end
---@param entry oil.Entry
---@param entry canola.Entry
---@return boolean
M.is_matching = function(entry)
-- if search highlightig is not enabled, all files are considered to match
@ -863,7 +864,7 @@ M.is_matching = function(entry)
if search_highlighting_is_off then
return true
end
local pattern = vim.fn.getreg("/")
local pattern = vim.fn.getreg('/')
local position_of_match = vim.fn.match(entry.name, pattern)
return position_of_match ~= -1
end
@ -874,11 +875,11 @@ 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
if vim.b[bufnr].canola_ready then
callback()
else
vim.api.nvim_create_autocmd("User", {
pattern = "OilEnter",
vim.api.nvim_create_autocmd('User', {
pattern = 'CanolaEnter',
callback = function(args)
if args.data.buf == bufnr then
vim.api.nvim_buf_call(bufnr, callback)
@ -889,25 +890,25 @@ M.run_after_load = function(bufnr, callback)
end
end
---@param entry oil.Entry
---@param entry canola.Entry
---@return boolean
M.is_directory = function(entry)
local is_directory = entry.type == "directory"
local is_directory = entry.type == 'directory'
or (
entry.type == "link"
entry.type == 'link'
and entry.meta
and entry.meta.link_stat
and entry.meta.link_stat.type == "directory"
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 bufnr integer The canola buffer that contains the entry
---@param entry canola.Entry
---@param callback fun(normalized_url: string)
M.get_edit_path = function(bufnr, entry, callback)
local pathutil = require("oil.pathutil")
local pathutil = require('canola.pathutil')
local bufname = vim.api.nvim_buf_get_name(bufnr)
local scheme, dir = M.parse_url(bufname)
@ -916,10 +917,10 @@ M.get_edit_path = function(bufnr, entry, callback)
local url = scheme .. dir .. entry.name
if M.is_directory(entry) then
url = url .. "/"
url = url .. '/'
end
if entry.name == ".." then
if entry.name == '..' then
callback(scheme .. pathutil.parent(dir))
elseif adapter.get_entry_path then
adapter.get_entry_path(url, entry, callback)
@ -929,45 +930,74 @@ M.get_edit_path = function(bufnr, entry, callback)
end
--- Check for an icon provider and return a common icon provider API
---@return (oil.IconProvider)?
---@return (canola.IconProvider)?
M.get_icon_provider = function()
-- prefer mini.icons
local _, mini_icons = pcall(require, "mini.icons")
local _, mini_icons = pcall(require, 'mini.icons')
-- selene: allow(global_usage)
---@diagnostic disable-next-line: undefined-field
if _G.MiniIcons then -- `_G.MiniIcons` is a better check to see if the module is setup
if _G.MiniIcons then
return function(type, name, conf, ft)
if ft then
return mini_icons.get("filetype", ft)
return mini_icons.get('filetype', ft)
end
return mini_icons.get(type == "directory" and "directory" or "file", name)
return mini_icons.get(type == 'directory' and 'directory' or 'file', name)
end
end
-- fallback to `nvim-web-devicons`
local has_devicons, devicons = pcall(require, "nvim-web-devicons")
if has_devicons then
local has_nonicons, nonicons = pcall(require, 'nonicons')
if has_nonicons and nonicons.get_icon then
local has_devicons, devicons = pcall(require, 'nvim-web-devicons')
if not has_devicons then
devicons = nil
end
return function(type, name, conf, ft)
if type == "directory" then
return conf and conf.directory or "", "OilDirIcon"
else
if ft then
local ft_icon, ft_hl = devicons.get_icon_by_filetype(ft)
if ft_icon and ft_icon ~= "" then
return ft_icon, ft_hl
end
end
local icon, hl = devicons.get_icon(name)
hl = hl or "OilFileIcon"
icon = icon or (conf and conf.default_file or "")
return icon, hl
if type == 'directory' then
local icon, hl = nonicons.get('file-directory-fill')
return icon or (conf and conf.directory or ''), hl or 'CanolaDirIcon'
end
if ft then
local ft_icon, ft_hl = nonicons.get_icon_by_filetype(ft)
if ft_icon then
return ft_icon, ft_hl or 'CanolaFileIcon'
end
end
local icon, hl = nonicons.get_icon(name)
if icon then
return icon, hl or 'CanolaFileIcon'
end
local fallback, fallback_hl = nonicons.get('file')
return fallback or (conf and conf.default_file or ''), fallback_hl or 'CanolaFileIcon'
end
end
local has_devicons, devicons = pcall(require, 'nvim-web-devicons')
if not has_devicons then
return
end
return function(type, name, conf, ft)
if type == 'directory' then
return conf and conf.directory or '', 'CanolaDirIcon'
else
if ft then
local ft_icon, ft_hl = devicons.get_icon_by_filetype(ft)
if ft_icon and ft_icon ~= '' then
return ft_icon, ft_hl
end
end
local icon, hl = devicons.get_icon(name)
hl = hl or 'CanolaFileIcon'
icon = icon or (conf and conf.default_file or '')
return icon, hl
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
---@param preview_method canola.PreviewMethod
---@return nil|integer
M.read_file_to_scratch_buffer = function(path, preview_method)
local bufnr = vim.api.nvim_create_buf(false, true)
@ -975,24 +1005,25 @@ M.read_file_to_scratch_buffer = function(path, preview_method)
return
end
vim.bo[bufnr].bufhidden = "wipe"
vim.bo[bufnr].buftype = "nofile"
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)
if preview_method == 'fast_scratch' then
has_lines, read_res = pcall(vim.fn.readfile, path, '', vim.o.lines)
else
has_lines, read_res = pcall(vim.fn.readfile, path)
end
local lines = has_lines and vim.split(table.concat(read_res, "\n"), "\n") or {}
local lines = has_lines and vim.split(table.concat(read_res, '\n'), '\n') or {}
local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines)
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
if ft and ft ~= '' and vim.treesitter.language.get_lang then
local lang = vim.treesitter.language.get_lang(ft)
-- selene: allow(empty_if)
if not pcall(vim.treesitter.start, bufnr, lang) then
vim.bo[bufnr].syntax = ft
else
@ -1000,8 +1031,8 @@ M.read_file_to_scratch_buffer = function(path, preview_method)
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",
vim.api.nvim_create_autocmd('BufEnter', {
desc = 'canola.nvim replace scratch buffer with real buffer',
buffer = bufnr,
callback = function()
local winid = vim.api.nvim_get_current_win()
@ -1013,8 +1044,8 @@ M.read_file_to_scratch_buffer = function(path, preview_method)
-- If we're still in a preview window, make sure this buffer still gets treated as a
-- preview
if vim.wo.previewwindow then
vim.bo.bufhidden = "wipe"
vim.b.oil_preview_buffer = true
vim.bo.bufhidden = 'wipe'
vim.b.canola_preview_buffer = true
end
end
end)
@ -1030,7 +1061,7 @@ local _regcache = {}
---@return boolean
M.file_matches_bufreadcmd = function(filename)
local autocmds = vim.api.nvim_get_autocmds({
event = "BufReadCmd",
event = 'BufReadCmd',
})
for _, au in ipairs(autocmds) do
local pat = _regcache[au.pattern]

View file

@ -1,12 +1,12 @@
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")
local cache = require('canola.cache')
local columns = require('canola.columns')
local config = require('canola.config')
local constants = require('canola.constants')
local fs = require('canola.fs')
local keymap_util = require('canola.keymap_util')
local loading = require('canola.loading')
local util = require('canola.util')
local M = {}
local FIELD_ID = constants.FIELD_ID
@ -18,7 +18,7 @@ local FIELD_META = constants.FIELD_META
local last_cursor_entry = {}
---@param bufnr integer
---@param entry oil.InternalEntry
---@param entry canola.InternalEntry
---@return boolean display
---@return boolean is_hidden Whether the file is classified as a hidden file
M.should_display = function(bufnr, entry)
@ -41,7 +41,7 @@ end
---Set the cursor to the last_cursor_entry if one exists
M.maybe_set_cursor = function()
local oil = require("oil")
local canola = require('canola')
local bufname = vim.api.nvim_buf_get_name(0)
local entry_name = last_cursor_entry[bufname]
if not entry_name then
@ -49,10 +49,10 @@ M.maybe_set_cursor = function()
end
local line_count = vim.api.nvim_buf_line_count(0)
for lnum = 1, line_count do
local entry = oil.get_entry_on_line(0, lnum)
local entry = canola.get_entry_on_line(0, lnum)
if entry and entry.name == entry_name then
local line = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1]
local id_str = line:match("^/(%d+)")
local id_str = line:match('^/(%d+)')
local col = line:find(entry_name, 1, true) or (id_str:len() + 1)
vim.api.nvim_win_set_cursor(0, { lnum, col - 1 })
M.set_last_cursor(bufname, nil)
@ -78,14 +78,14 @@ local function are_any_modified()
end
local function is_unix_executable(entry)
if entry[FIELD_TYPE] == "directory" then
if entry[FIELD_TYPE] == 'directory' then
return false
end
local meta = entry[FIELD_META]
if not meta or not meta.stat then
return false
end
if meta.stat.type == "directory" then
if meta.stat.type == 'directory' then
return false
end
@ -98,51 +98,51 @@ end
M.toggle_hidden = function()
local any_modified = are_any_modified()
if any_modified then
vim.notify("Cannot toggle hidden files when you have unsaved changes", vim.log.levels.WARN)
vim.notify('Cannot toggle hidden files when you have unsaved changes', vim.log.levels.WARN)
else
config.view_options.show_hidden = not config.view_options.show_hidden
M.rerender_all_oil_buffers({ refetch = false })
M.rerender_all_canola_buffers({ refetch = false })
end
end
---@param is_hidden_file fun(filename: string, bufnr: integer, entry: oil.Entry): boolean
---@param is_hidden_file fun(filename: string, bufnr: integer, entry: canola.Entry): boolean
M.set_is_hidden_file = function(is_hidden_file)
local any_modified = are_any_modified()
if any_modified then
vim.notify("Cannot change is_hidden_file when you have unsaved changes", vim.log.levels.WARN)
vim.notify('Cannot change is_hidden_file when you have unsaved changes', vim.log.levels.WARN)
else
config.view_options.is_hidden_file = is_hidden_file
M.rerender_all_oil_buffers({ refetch = false })
M.rerender_all_canola_buffers({ refetch = false })
end
end
M.set_columns = function(cols)
local any_modified = are_any_modified()
if any_modified then
vim.notify("Cannot change columns when you have unsaved changes", vim.log.levels.WARN)
vim.notify('Cannot change columns when you have unsaved changes', vim.log.levels.WARN)
else
config.columns = cols
-- TODO only refetch if we don't have all the necessary data for the columns
M.rerender_all_oil_buffers({ refetch = true })
M.rerender_all_canola_buffers({ refetch = true })
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)
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 })
M.rerender_all_canola_buffers({ refetch = true })
end
end
---@class oil.ViewData
---@class canola.ViewData
---@field fs_event? any uv_fs_event_t
-- List of bufnrs
---@type table<integer, oil.ViewData>
---@type table<integer, canola.ViewData>
local session = {}
---@return integer[]
@ -151,7 +151,7 @@ M.get_all_buffers = function()
end
local buffers_locked = false
---Make all oil buffers nomodifiable
---Make all canola buffers nomodifiable
M.lock_buffers = function()
buffers_locked = true
for bufnr in pairs(session) do
@ -161,7 +161,7 @@ M.lock_buffers = function()
end
end
---Restore normal modifiable settings for oil buffers
---Restore normal modifiable settings for canola buffers
M.unlock_buffers = function()
buffers_locked = false
for bufnr in pairs(session) do
@ -177,8 +177,8 @@ end
---@param opts? table
---@param callback? fun(err: nil|string)
---@note
--- This DISCARDS ALL MODIFICATIONS a user has made to oil buffers
M.rerender_all_oil_buffers = function(opts, callback)
--- This DISCARDS ALL MODIFICATIONS a user has made to canola buffers
M.rerender_all_canola_buffers = function(opts, callback)
opts = opts or {}
local buffers = M.get_all_buffers()
local hidden_buffers = {}
@ -193,7 +193,7 @@ M.rerender_all_oil_buffers = function(opts, callback)
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
vim.b[bufnr].canola_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)
@ -207,19 +207,19 @@ 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 })
vim.api.nvim_set_option_value('foldmethod', 'manual', { scope = 'local', win = winid })
for k, v in pairs(config.win_options) do
vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid })
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 })
vim.api.nvim_set_option_value(k, v, { scope = 'local', win = winid })
end
end
end
---Get a list of visible oil buffers and a list of hidden oil buffers
---Get a list of visible canola buffers and a list of hidden canola buffers
---@note
--- If any buffers are modified, return values are nil
---@return nil|integer[] visible
@ -244,14 +244,14 @@ local function get_visible_hidden_buffers()
return visible_buffers, vim.tbl_keys(hidden_buffers)
end
---Delete unmodified, hidden oil buffers and if none remain, clear the cache
---Delete unmodified, hidden canola 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"
or vim.fn.win_gettype() == 'command'
then
return
end
@ -261,7 +261,7 @@ M.delete_hidden_buffers = function()
cache.clear_everything()
end
---@param adapter oil.Adapter
---@param adapter canola.Adapter
---@param ranges table<string, integer[]>
---@return integer
local function get_first_mutable_column_col(adapter, ranges)
@ -278,20 +278,20 @@ local function get_first_mutable_column_col(adapter, ranges)
end
--- @param bufnr integer
--- @param adapter oil.Adapter
--- @param adapter canola.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 parser = require('canola.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
if mode == 'editable' then
min_col = get_first_mutable_column_col(adapter, result.ranges)
elseif mode == "name" then
elseif mode == 'name' then
min_col = result.ranges.name[1]
else
error(string.format('Unexpected value "%s" for option constrain_cursor', mode))
@ -318,7 +318,7 @@ local function constrain_cursor(bufnr, mode)
return
end
local mc = package.loaded["multicursor-nvim"]
local mc = package.loaded['multicursor-nvim']
if mc then
mc.onSafeState(function()
mc.action(function(ctx)
@ -340,20 +340,98 @@ local function constrain_cursor(bufnr, mode)
end
end
---@param bufnr integer
local function show_insert_guide(bufnr)
if not config.constrain_cursor 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 cur = vim.api.nvim_win_get_cursor(0)
local current_line = vim.api.nvim_buf_get_lines(bufnr, cur[1] - 1, cur[1], true)[1]
if current_line ~= '' then
return
end
local all_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
local ref_line
if cur[1] > 1 and all_lines[cur[1] - 1] ~= '' then
ref_line = all_lines[cur[1] - 1]
elseif cur[1] < #all_lines and all_lines[cur[1] + 1] ~= '' then
ref_line = all_lines[cur[1] + 1]
else
for i, line in ipairs(all_lines) do
if line ~= '' and i ~= cur[1] then
ref_line = line
break
end
end
end
if not ref_line then
return
end
local parser = require('canola.mutator.parser')
local column_defs = columns.get_supported_columns(adapter)
local result = parser.parse_line(adapter, ref_line, column_defs)
if not result or not result.ranges then
return
end
local id_end = result.ranges.id[2] + 1
local col_prefix = ref_line:sub(id_end + 1, result.ranges.name[1])
local col_width = vim.api.nvim_strwidth(col_prefix)
local id_width
local cole = vim.wo.conceallevel
if cole >= 2 then
id_width = 0
elseif cole == 1 then
id_width = 1
else
id_width = vim.api.nvim_strwidth(ref_line:sub(1, id_end))
end
local virtual_col = id_width + col_width
if virtual_col <= 0 then
return
end
vim.w.canola_saved_ve = vim.wo.virtualedit
vim.wo.virtualedit = 'all'
vim.api.nvim_win_set_cursor(0, { cur[1], virtual_col })
vim.api.nvim_create_autocmd('TextChangedI', {
group = 'Canola',
buffer = bufnr,
once = true,
callback = function()
if vim.w.canola_saved_ve ~= nil then
vim.wo.virtualedit = vim.w.canola_saved_ve
vim.w.canola_saved_ve = nil
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 parser = require('canola.mutator.parser')
local adapter = util.get_adapter(bufnr, true)
if not adapter or adapter.name ~= "trash" then
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")
local ns = vim.api.nvim_create_namespace('CanolaVtext')
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
@ -361,14 +439,14 @@ local function redraw_trash_virtual_text(bufnr)
local entry = result and result.entry
if entry then
local meta = entry[FIELD_META]
---@type nil|oil.TrashInfo
---@type nil|canola.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",
'' .. fs.shorten_path(trash_info.original_path, os_path),
'CanolaTrashSourcePath',
},
},
})
@ -387,13 +465,13 @@ M.initialize = function(bufnr)
end
vim.api.nvim_clear_autocmds({
buffer = bufnr,
group = "Oil",
group = 'Canola',
})
vim.bo[bufnr].buftype = "acwrite"
vim.bo[bufnr].buftype = 'acwrite'
vim.bo[bufnr].readonly = false
vim.bo[bufnr].swapfile = false
vim.bo[bufnr].syntax = "oil"
vim.bo[bufnr].filetype = "oil"
vim.bo[bufnr].syntax = 'canola'
vim.bo[bufnr].filetype = 'canola'
vim.b[bufnr].EditorConfig_disable = 1
session[bufnr] = session[bufnr] or {}
for k, v in pairs(config.buf_options) do
@ -401,19 +479,19 @@ M.initialize = function(bufnr)
end
vim.api.nvim_buf_call(bufnr, M.set_win_options)
vim.api.nvim_create_autocmd("BufHidden", {
desc = "Delete oil buffers when no longer in use",
group = "Oil",
vim.api.nvim_create_autocmd('BufHidden', {
desc = 'Delete canola buffers when no longer in use',
group = 'Canola',
nested = true,
buffer = bufnr,
callback = function()
-- First wait a short time (100ms) 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 delete canola buffers if none of them are visible
if visible_buffers and vim.tbl_isempty(visible_buffers) then
-- Check if cleanup is enabled
if type(config.cleanup_delay_ms) == "number" then
if type(config.cleanup_delay_ms) == 'number' then
if config.cleanup_delay_ms > 0 then
vim.defer_fn(function()
M.delete_hidden_buffers()
@ -426,8 +504,8 @@ M.initialize = function(bufnr)
end, 100)
end,
})
vim.api.nvim_create_autocmd("BufUnload", {
group = "Oil",
vim.api.nvim_create_autocmd('BufUnload', {
group = 'Canola',
nested = true,
once = true,
buffer = bufnr,
@ -439,34 +517,47 @@ M.initialize = function(bufnr)
end
end,
})
vim.api.nvim_create_autocmd("BufEnter", {
group = "Oil",
vim.api.nvim_create_autocmd('BufEnter', {
group = 'Canola',
buffer = bufnr,
callback = function(args)
local opts = vim.b[args.buf].oil_dirty
local opts = vim.b[args.buf].canola_dirty
if opts then
vim.b[args.buf].oil_dirty = nil
vim.b[args.buf].canola_dirty = nil
M.render_buffer_async(args.buf, opts)
end
end,
})
local timer
vim.api.nvim_create_autocmd("InsertEnter", {
desc = "Constrain oil cursor position",
group = "Oil",
vim.api.nvim_create_autocmd('InsertEnter', {
desc = 'Constrain canola cursor position',
group = 'Canola',
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)
vim.schedule(function()
constrain_cursor(bufnr, config.constrain_cursor)
show_insert_guide(bufnr)
end)
end,
})
vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, {
desc = "Update oil preview window",
group = "Oil",
vim.api.nvim_create_autocmd('InsertLeave', {
group = 'Canola',
buffer = bufnr,
callback = function()
local oil = require("oil")
if vim.w.canola_saved_ve ~= nil then
vim.wo.virtualedit = vim.w.canola_saved_ve
vim.w.canola_saved_ve = nil
end
end,
})
vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI', 'ModeChanged' }, {
desc = 'Update canola preview window',
group = 'Canola',
buffer = bufnr,
callback = function()
local canola = require('canola')
if vim.wo.previewwindow then
return
end
@ -491,14 +582,14 @@ M.initialize = function(bufnr)
if vim.api.nvim_get_current_buf() ~= bufnr then
return
end
local entry = oil.get_cursor_entry()
local entry = canola.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()
if entry.id ~= vim.w[winid].canola_entry_id then
canola.open_preview()
end
end
end
@ -513,7 +604,7 @@ M.initialize = function(bufnr)
-- Set up a watcher that will refresh the directory
if
adapter
and adapter.name == "files"
and adapter.name == 'files'
and config.watch_for_changes
and not session[bufnr].fs_event
then
@ -532,8 +623,8 @@ M.initialize = function(bufnr)
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
local mutator = require('canola.mutator')
if err or vim.bo[bufnr].modified or vim.b[bufnr].canola_dirty or mutator.is_mutating() then
return
end
@ -546,18 +637,18 @@ M.initialize = function(bufnr)
end
-- If it is not currently visible, mark it as dirty
vim.b[bufnr].oil_dirty = {}
vim.b[bufnr].canola_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
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",
vim.api.nvim_create_autocmd('TextChanged', {
desc = 'Update canola virtual text of original path',
buffer = bufnr,
callback = function()
-- Respond immediately to prevent flickering, the set the timer for a "cooldown period"
@ -583,35 +674,35 @@ M.initialize = function(bufnr)
M.render_buffer_async(bufnr, {}, function(err)
if err then
vim.notify(
string.format("Error rendering oil buffer %s: %s", vim.api.nvim_buf_get_name(bufnr), err),
string.format('Error rendering canola buffer %s: %s', vim.api.nvim_buf_get_name(bufnr), err),
vim.log.levels.ERROR
)
else
vim.b[bufnr].oil_ready = true
vim.b[bufnr].canola_ready = true
vim.api.nvim_exec_autocmds(
"User",
{ pattern = "OilEnter", modeline = false, data = { buf = bufnr } }
'User',
{ pattern = 'CanolaEnter', modeline = false, data = { buf = bufnr } }
)
end
end)
keymap_util.set_keymaps(config.keymaps, bufnr)
end
---@param adapter oil.Adapter
---@param adapter canola.Adapter
---@param num_entries integer
---@return fun(a: oil.InternalEntry, b: oil.InternalEntry): boolean
---@return fun(a: canola.InternalEntry, b: canola.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" } }
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
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'",
@ -639,7 +730,7 @@ local function get_sort_function(adapter, num_entries)
local a_val = get_sort_value(a)
local b_val = get_sort_value(b)
if a_val ~= b_val then
if order == "desc" then
if order == 'desc' then
return a_val > b_val
else
return a_val < b_val
@ -663,7 +754,7 @@ local function render_buffer(bufnr, opts)
return false
end
local bufname = vim.api.nvim_buf_get_name(bufnr)
opts = vim.tbl_extend("keep", opts or {}, {
opts = vim.tbl_extend('keep', opts or {}, {
jump = false,
jump_first = false,
})
@ -676,7 +767,7 @@ local function render_buffer(bufnr, opts)
local entry_list = vim.tbl_values(entries)
-- Only sort the entries once we have them all
if not vim.b[bufnr].oil_rendering then
if not vim.b[bufnr].canola_rendering then
table.sort(entry_list, get_sort_function(adapter, #entry_list))
end
@ -693,10 +784,10 @@ local function render_buffer(bufnr, opts)
for i, col_def in ipairs(column_defs) do
col_width[i + 1] = 1
local _, conf = util.split_config(col_def)
col_align[i + 1] = conf and conf.align or "left"
col_align[i + 1] = conf and conf.align or 'left'
end
local parent_entry = { 0, "..", "directory" }
local parent_entry = { 0, '..', 'directory' }
if M.should_display(bufnr, parent_entry) then
local cols = M.format_entry_cols(parent_entry, column_defs, col_width, adapter, true, bufnr)
table.insert(line_table, cols)
@ -716,6 +807,21 @@ local function render_buffer(bufnr, opts)
end
end
if config.view_options.show_hidden_when_empty and #line_table <= 1 then
for _, entry in ipairs(entry_list) do
local name = entry[FIELD_NAME]
local public_entry = util.export_entry(entry)
if not config.view_options.is_always_hidden(name, bufnr, public_entry) then
local cols = M.format_entry_cols(entry, column_defs, col_width, adapter, true, bufnr)
table.insert(line_table, cols)
if seek_after_render == name then
seek_after_render_found = true
jump_idx = #line_table
end
end
end
end
local lines, highlights = util.render_table(line_table, col_width, col_align)
vim.bo[bufnr].modifiable = true
@ -732,7 +838,7 @@ local function render_buffer(bufnr, opts)
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_str = line:match('^/(%d+)')
local id = tonumber(id_str)
if id then
local entry = cache.get_entry_by_id(id)
@ -745,7 +851,7 @@ local function render_buffer(bufnr, opts)
end
end
constrain_cursor(bufnr, "name")
constrain_cursor(bufnr, 'name')
end
end
end)
@ -760,13 +866,13 @@ end
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 .. "/"
if meta.link_stat and meta.link_stat.type == 'directory' then
name = name .. '/'
end
if meta.link then
link_text = "-> " .. meta.link:gsub("\n", "")
if meta.link_stat and meta.link_stat.type == "directory" then
link_text = '-> ' .. meta.link:gsub('\n', '')
if meta.link_stat and meta.link_stat.type == 'directory' then
link_text = util.addslash(link_text)
end
end
@ -776,25 +882,25 @@ local function get_link_text(name, meta)
end
---@private
---@param entry oil.InternalEntry
---@param entry canola.InternalEntry
---@param column_defs table[]
---@param col_width integer[]
---@param adapter oil.Adapter
---@param adapter canola.Adapter
---@param is_hidden boolean
---@param bufnr integer
---@return oil.TextChunk[]
---@return canola.TextChunk[]
M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden, bufnr)
local name = entry[FIELD_NAME]
local meta = entry[FIELD_META]
local hl_suffix = ""
local hl_suffix = ''
if is_hidden then
hl_suffix = "Hidden"
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", "")
name = name:gsub('\n', '')
-- First put the unique ID
local cols = {}
local id_key = cache.format_id(entry[FIELD_ID])
@ -803,7 +909,7 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
-- Then add all the configured columns
for i, column in ipairs(column_defs) do
local chunk = columns.render_col(adapter, column, entry, bufnr)
local text = type(chunk) == "table" and chunk[1] or chunk
local text = type(chunk) == 'table' and chunk[1] or chunk
---@cast text string
col_width[i + 1] = math.max(col_width[i + 1], vim.api.nvim_strwidth(text))
table.insert(cols, chunk)
@ -816,7 +922,7 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
if get_custom_hl then
local external_entry = util.export_entry(entry)
if entry_type == "link" then
if entry_type == 'link' then
link_name, link_target = get_link_text(name, meta)
local is_orphan = not (meta and meta.link_stat)
link_name_hl = get_custom_hl(external_entry, is_hidden, false, is_orphan, bufnr)
@ -830,8 +936,8 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
local hl = get_custom_hl(external_entry, is_hidden, false, false, bufnr)
if hl then
-- Add the trailing / if this is a directory, this is important
if entry_type == "directory" then
name = name .. "/"
if entry_type == 'directory' then
name = name .. '/'
end
table.insert(cols, { name, hl })
return cols
@ -840,49 +946,50 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
end
local highlight_as_executable = false
if entry_type ~= "directory" then
if entry_type ~= 'directory' then
local lower = name:lower()
if
lower:match("%.exe$")
or lower:match("%.bat$")
or lower:match("%.cmd$")
or lower:match("%.com$")
or lower:match("%.ps1$")
lower:match('%.exe$')
or lower:match('%.bat$')
or lower:match('%.cmd$')
or lower:match('%.com$')
or lower:match('%.ps1$')
then
highlight_as_executable = true
-- selene: allow(if_same_then_else)
elseif is_unix_executable(entry) then
highlight_as_executable = true
end
end
if entry_type == "directory" then
table.insert(cols, { name .. "/", "OilDir" .. hl_suffix })
elseif entry_type == "socket" then
table.insert(cols, { name, "OilSocket" .. hl_suffix })
elseif entry_type == "link" then
if entry_type == 'directory' then
table.insert(cols, { name .. '/', 'CanolaDir' .. hl_suffix })
elseif entry_type == 'socket' then
table.insert(cols, { name, 'CanolaSocket' .. hl_suffix })
elseif entry_type == 'link' then
if not link_name then
link_name, link_target = get_link_text(name, meta)
end
local is_orphan = not (meta and meta.link_stat)
if not link_name_hl then
if highlight_as_executable then
link_name_hl = "OilExecutable" .. hl_suffix
link_name_hl = 'CanolaExecutable' .. hl_suffix
else
link_name_hl = (is_orphan and "OilOrphanLink" or "OilLink") .. hl_suffix
link_name_hl = (is_orphan and 'CanolaOrphanLink' or 'CanolaLink') .. hl_suffix
end
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
link_target_hl = (is_orphan and 'CanolaOrphanLinkTarget' or 'CanolaLinkTarget') .. hl_suffix
end
table.insert(cols, { link_target, link_target_hl })
end
elseif highlight_as_executable then
table.insert(cols, { name, "OilExecutable" .. hl_suffix })
table.insert(cols, { name, 'CanolaExecutable' .. hl_suffix })
else
table.insert(cols, { name, "OilFile" .. hl_suffix })
table.insert(cols, { name, 'CanolaFile' .. hl_suffix })
end
return cols
@ -914,8 +1021,8 @@ M.render_buffer_async = function(bufnr, opts, caller_callback)
local function callback(err)
if not err then
vim.api.nvim_exec_autocmds(
"User",
{ pattern = "OilReadPost", modeline = false, data = { buf = bufnr } }
'User',
{ pattern = 'CanolaReadPost', modeline = false, data = { buf = bufnr } }
)
end
if caller_callback then
@ -923,7 +1030,7 @@ M.render_buffer_async = function(bufnr, opts, caller_callback)
end
end
opts = vim.tbl_deep_extend("keep", opts or {}, {
opts = vim.tbl_deep_extend('keep', opts or {}, {
refetch = true,
})
---@cast opts -nil
@ -932,7 +1039,7 @@ M.render_buffer_async = function(bufnr, opts, caller_callback)
end
-- If we're already rendering, queue up another rerender after it's complete
if vim.b[bufnr].oil_rendering then
if vim.b[bufnr].canola_rendering then
if not pending_renders[bufnr] then
pending_renders[bufnr] = { callback }
elseif callback then
@ -942,15 +1049,15 @@ M.render_buffer_async = function(bufnr, opts, caller_callback)
end
local bufname = vim.api.nvim_buf_get_name(bufnr)
vim.b[bufnr].oil_rendering = true
vim.b[bufnr].canola_rendering = true
local _, dir = util.parse_url(bufname)
-- Undo should not return to a blank buffer
-- Method taken from :h clear-undo
vim.bo[bufnr].undolevels = -1
local handle_error = vim.schedule_wrap(function(message)
vim.b[bufnr].oil_rendering = false
vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" })
util.render_text(bufnr, { "Error: " .. message })
vim.b[bufnr].canola_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)
@ -964,12 +1071,12 @@ M.render_buffer_async = function(bufnr, opts, caller_callback)
end
end)
if not dir then
handle_error(string.format("Could not parse oil url '%s'", bufname))
handle_error(string.format("Could not parse canola url '%s'", bufname))
return
end
local adapter = util.get_adapter(bufnr, true)
if not adapter then
handle_error(string.format("[oil] no adapter for buffer '%s'", bufname))
handle_error(string.format("[canola] no adapter for buffer '%s'", bufname))
return
end
local start_ms = uv.hrtime() / 1e6
@ -983,11 +1090,11 @@ M.render_buffer_async = function(bufnr, opts, caller_callback)
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end
vim.b[bufnr].oil_rendering = false
vim.b[bufnr].canola_rendering = false
loading.set_loading(bufnr, false)
render_buffer(bufnr, { jump = true })
M.set_last_cursor(bufname, nil)
vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" })
vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value('undolevels', { scope = 'global' })
vim.bo[bufnr].modifiable = not buffers_locked and adapter.is_modifiable(bufnr)
if callback then
callback()

View file

@ -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

View file

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

View file

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

View file

@ -1,11 +1,11 @@
vim.opt.runtimepath:prepend("scripts/benchmark.nvim")
vim.opt.runtimepath:prepend(".")
vim.opt.runtimepath:prepend('scripts/benchmark.nvim')
vim.opt.runtimepath:prepend('.')
local bm = require("benchmark")
local bm = require('benchmark')
bm.sandbox()
---@module 'oil'
---@type oil.SetupOpts
---@module 'canola'
---@type canola.SetupOpts
local setup_opts = {
-- columns = { "icon", "permissions", "size", "mtime" },
}
@ -14,50 +14,53 @@ 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
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)
vim.fn.mkdir(TEST_DIR, 'p')
require('benchmark.files').create_files(TEST_DIR, 'file %d.txt', DIR_SIZE)
-- selene: allow(global_usage)
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()
require('canola').setup(setup_opts)
local finish = bm.jit_profile({ filename = TEST_DIR .. '/profile.txt' })
bm.wait_for_user_event('CanolaEnter', function()
finish()
end)
require("oil").open(TEST_DIR)
require('canola').open(TEST_DIR)
end
-- selene: allow(global_usage)
function _G.flame_profile()
local start, stop = bm.flame_profile({
pattern = "oil*",
filename = "profile.json",
pattern = 'canola*',
filename = 'profile.json',
})
require("oil").setup(setup_opts)
require('canola').setup(setup_opts)
start()
bm.wait_for_user_event("OilEnter", function()
bm.wait_for_user_event('CanolaEnter', function()
stop(function()
vim.cmd.qall({ mods = { silent = true } })
end)
end)
require("oil").open(TEST_DIR)
require('canola').open(TEST_DIR)
end
-- selene: allow(global_usage)
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)
require('canola').setup(setup_opts)
bm.run({ title = 'canola.nvim', iterations = ITERATIONS, warm_up = WARM_UP }, function(callback)
bm.wait_for_user_event('CanolaEnter', callback)
require('canola').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)),
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.fn.writefile(lines, 'perf/tmp/benchmark.txt')
vim.cmd.qall({ mods = { silent = true } })
end)
end

3
plugin/canola.lua Normal file
View file

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

View file

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

View file

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

9
scripts/ci.sh Executable file
View file

@ -0,0 +1,9 @@
#!/bin/sh
set -eu
nix develop --command stylua --check lua spec
git ls-files '*.lua' | xargs nix develop --command selene --display-style quiet
nix develop --command prettier --check .
nix fmt
git diff --exit-code -- '*.nix'
nix develop --command busted

7
selene.toml Normal file
View file

@ -0,0 +1,7 @@
std = 'vim'
exclude = [".direnv/*"]
[lints]
mixed_table = 'allow'
unused_variable = 'allow'
bad_string_escape = 'allow'

150
spec/altbuf_spec.lua Normal file
View file

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

44
spec/close_spec.lua Normal file
View file

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

27
spec/config_spec.lua Normal file
View file

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

209
spec/files_spec.lua Normal file
View file

@ -0,0 +1,209 @@
local TmpDir = require('spec.tmpdir')
local files = require('canola.adapters.files')
local test_util = require('spec.test_util')
describe('files adapter', function()
local tmpdir
before_each(function()
tmpdir = TmpDir.new()
end)
after_each(function()
if tmpdir then
tmpdir:dispose()
end
test_util.reset_editor()
end)
it('tmpdir creates files and asserts they exist', function()
tmpdir:create({ 'a.txt', 'foo/b.txt', 'foo/c.txt', 'bar/' })
tmpdir:assert_fs({
['a.txt'] = 'a.txt',
['foo/b.txt'] = 'foo/b.txt',
['foo/c.txt'] = 'foo/c.txt',
['bar/'] = true,
})
end)
it('Creates files', function()
local err = test_util.await(files.perform_action, 2, {
url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a.txt',
entry_type = 'file',
type = 'create',
})
assert.is_nil(err)
tmpdir:assert_fs({
['a.txt'] = '',
})
end)
it('Creates directories', function()
local err = test_util.await(files.perform_action, 2, {
url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a',
entry_type = 'directory',
type = 'create',
})
assert.is_nil(err)
tmpdir:assert_fs({
['a/'] = true,
})
end)
it('Deletes files', function()
tmpdir:create({ 'a.txt' })
local url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a.txt'
local err = test_util.await(files.perform_action, 2, {
url = url,
entry_type = 'file',
type = 'delete',
})
assert.is_nil(err)
tmpdir:assert_fs({})
end)
it('Deletes directories', function()
tmpdir:create({ 'a/' })
local url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a'
local err = test_util.await(files.perform_action, 2, {
url = url,
entry_type = 'directory',
type = 'delete',
})
assert.is_nil(err)
tmpdir:assert_fs({})
end)
it('Moves files', function()
tmpdir:create({ 'a.txt' })
local src_url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a.txt'
local dest_url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'b.txt'
local err = test_util.await(files.perform_action, 2, {
src_url = src_url,
dest_url = dest_url,
entry_type = 'file',
type = 'move',
})
assert.is_nil(err)
tmpdir:assert_fs({
['b.txt'] = 'a.txt',
})
end)
it('Moves directories', function()
tmpdir:create({ 'a/a.txt' })
local src_url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a'
local dest_url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'b'
local err = test_util.await(files.perform_action, 2, {
src_url = src_url,
dest_url = dest_url,
entry_type = 'directory',
type = 'move',
})
assert.is_nil(err)
tmpdir:assert_fs({
['b/a.txt'] = 'a/a.txt',
['b/'] = true,
})
end)
it('Copies files', function()
tmpdir:create({ 'a.txt' })
local src_url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a.txt'
local dest_url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'b.txt'
local err = test_util.await(files.perform_action, 2, {
src_url = src_url,
dest_url = dest_url,
entry_type = 'file',
type = 'copy',
})
assert.is_nil(err)
tmpdir:assert_fs({
['a.txt'] = 'a.txt',
['b.txt'] = 'a.txt',
})
end)
it('Recursively copies directories', function()
tmpdir:create({ 'a/a.txt' })
local src_url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a'
local dest_url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'b'
local err = test_util.await(files.perform_action, 2, {
src_url = src_url,
dest_url = dest_url,
entry_type = 'directory',
type = 'copy',
})
assert.is_nil(err)
tmpdir:assert_fs({
['b/a.txt'] = 'a/a.txt',
['b/'] = true,
['a/a.txt'] = 'a/a.txt',
['a/'] = true,
})
end)
it('Editing a new canola://path/ creates an canola buffer', function()
local tmpdir_url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. '/'
vim.cmd.edit({ args = { tmpdir_url } })
test_util.wait_canola_ready()
local new_url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'newdir'
vim.cmd.edit({ args = { new_url } })
test_util.wait_canola_ready()
assert.equals('canola', vim.bo.filetype)
assert.equals(new_url .. '/', vim.api.nvim_buf_get_name(0))
end)
it('Editing a new canola://file.rb creates a normal buffer', function()
local tmpdir_url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. '/'
vim.cmd.edit({ args = { tmpdir_url } })
test_util.wait_for_autocmd('BufReadPost')
local new_url = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p') .. 'file.rb'
vim.cmd.edit({ args = { new_url } })
test_util.wait_for_autocmd('BufReadPost')
assert.equals('ruby', vim.bo.filetype)
assert.equals(vim.fn.fnamemodify(tmpdir.path, ':p') .. 'file.rb', vim.api.nvim_buf_get_name(0))
assert.equals(tmpdir.path .. '/file.rb', vim.fn.bufname())
end)
describe('cleanup_buffers_on_delete', function()
local cache = require('canola.cache')
local config = require('canola.config')
local mutator = require('canola.mutator')
before_each(function()
config.cleanup_buffers_on_delete = true
end)
after_each(function()
config.cleanup_buffers_on_delete = false
end)
it('wipes the buffer for a deleted file', function()
tmpdir:create({ 'a.txt' })
local dirurl = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p')
local filepath = vim.fn.fnamemodify(tmpdir.path, ':p') .. 'a.txt'
cache.create_and_store_entry(dirurl, 'a.txt', 'file')
vim.cmd.edit({ args = { filepath } })
local bufnr = vim.api.nvim_get_current_buf()
local url = 'canola://' .. filepath
test_util.await(mutator.process_actions, 2, {
{ type = 'delete', url = url, entry_type = 'file' },
})
assert.is_false(vim.api.nvim_buf_is_valid(bufnr))
end)
it('does not wipe the buffer when disabled', function()
config.cleanup_buffers_on_delete = false
tmpdir:create({ 'b.txt' })
local dirurl = 'canola://' .. vim.fn.fnamemodify(tmpdir.path, ':p')
local filepath = vim.fn.fnamemodify(tmpdir.path, ':p') .. 'b.txt'
cache.create_and_store_entry(dirurl, 'b.txt', 'file')
vim.cmd.edit({ args = { filepath } })
local bufnr = vim.api.nvim_get_current_buf()
local url = 'canola://' .. filepath
test_util.await(mutator.process_actions, 2, {
{ type = 'delete', url = url, entry_type = 'file' },
})
assert.is_true(vim.api.nvim_buf_is_valid(bufnr))
end)
end)
end)

View file

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

9
spec/minimal_init.lua Normal file
View file

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

59
spec/move_rename_spec.lua Normal file
View file

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

419
spec/mutator_spec.lua Normal file
View file

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

249
spec/parser_spec.lua Normal file
View file

@ -0,0 +1,249 @@
local constants = require('canola.constants')
local parser = require('canola.mutator.parser')
local test_adapter = require('canola.adapters.test')
local test_util = require('spec.test_util')
local util = require('canola.util')
local view = require('canola.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 = { 'canola-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 = { 'canola-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 = { 'canola-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 = { 'canola-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 = { 'canola-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 = { 'canola-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 = { 'canola-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 = { 'canola-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 = { 'canola-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 = { 'canola-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 = { 'canola-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 = { 'canola-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 = { 'canola-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 = { 'canola-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 = { 'canola-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 = { 'canola-test:///foo/' } })
local bufnr = vim.api.nvim_get_current_buf()
set_lines(bufnr, {
string.format('/%d mydir/ -> dir/', file[FIELD_ID]),
})
local diffs = parser.parse(bufnr)
assert.are.same({}, diffs)
end)
end)

View file

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

40
spec/preview_spec.lua Normal file
View file

@ -0,0 +1,40 @@
local TmpDir = require('spec.tmpdir')
local canola = require('canola')
local test_util = require('spec.test_util')
local util = require('canola.util')
describe('canola preview', function()
local tmpdir
before_each(function()
tmpdir = TmpDir.new()
end)
after_each(function()
if tmpdir then
tmpdir:dispose()
end
test_util.reset_editor()
end)
it('opens preview window', function()
tmpdir:create({ 'a.txt' })
test_util.canola_open(tmpdir.path)
test_util.await(canola.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)
it('opens preview window when open(preview={})', function()
tmpdir:create({ 'a.txt' })
test_util.canola_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)

137
spec/regression_spec.lua Normal file
View file

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

View file

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

172
spec/test_util.lua Normal file
View file

@ -0,0 +1,172 @@
local cache = require('canola.cache')
local test_adapter = require('canola.adapters.test')
local util = require('canola.util')
local M = {}
M.reset_editor = function()
require('canola').setup({
columms = {},
adapters = {
['canola-test://'] = 'test',
},
prompt_save_on_select_new_entry = false,
})
vim.cmd.tabonly({ mods = { silent = true } })
vim.cmd.new()
vim.cmd.only()
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
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.await = function(fn, nargs, ...)
local done = false
local results
local n_results = 0
local args = { ... }
args[nargs] = function(...)
results = { ... }
n_results = select('#', ...)
done = true
end
fn(unpack(args, 1, nargs))
vim.wait(10000, function()
return done
end, 10)
if not done then
error('M.await timed out')
end
return unpack(results, 1, n_results)
end
M.await_throwiferr = function(fn, nargs, ...)
return throwiferr(M.await(fn, nargs, ...))
end
M.canola_open = function(...)
M.await(require('canola').open, 3, ...)
end
M.wait_for_autocmd = function(autocmd)
local triggered = false
local opts = {
pattern = '*',
nested = true,
once = true,
}
if type(autocmd) == 'table' then
opts = vim.tbl_extend('force', opts, autocmd)
autocmd = autocmd[1]
opts[1] = nil
end
opts.callback = vim.schedule_wrap(function()
triggered = true
end)
vim.api.nvim_create_autocmd(autocmd, opts)
vim.wait(10000, function()
return triggered
end, 10)
if not triggered then
error('wait_for_autocmd timed out waiting for ' .. tostring(autocmd))
end
end
M.wait_canola_ready = function()
local ready = false
util.run_after_load(
0,
vim.schedule_wrap(function()
ready = true
end)
)
vim.wait(10000, function()
return ready
end, 10)
if not ready then
error('wait_canola_ready timed out')
end
end
---@param actions string[]
---@param timestep integer
M.feedkeys = function(actions, timestep)
timestep = timestep or 10
vim.wait(timestep)
for _, action in ipairs(actions) do
vim.wait(timestep)
local escaped = vim.api.nvim_replace_termcodes(action, true, false, true)
vim.api.nvim_feedkeys(escaped, 'm', true)
end
vim.wait(timestep)
vim.api.nvim_feedkeys('', 'x', true)
vim.wait(timestep)
end
M.actions = {
---Open canola and wait for it to finish rendering
---@param args string[]
open = function(args)
vim.schedule(function()
vim.cmd.Canola({ args = args })
if vim.b.canola_ready then
vim.api.nvim_exec_autocmds('User', {
pattern = 'CanolaEnter',
modeline = false,
data = { buf = vim.api.nvim_get_current_buf() },
})
end
end)
M.wait_for_autocmd({ 'User', pattern = 'CanolaEnter' })
end,
---Save all changes and wait for operation to complete
save = function()
vim.schedule_wrap(require('canola').save)({ confirm = false })
M.wait_for_autocmd({ 'User', pattern = 'CanolaMutationComplete' })
end,
---@param bufnr? integer
reload = function(bufnr)
M.await(require('canola.view').render_buffer_async, 3, bufnr or 0)
end,
---Move cursor to a file or directory in an canola 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 canola 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 canola 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

View file

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

149
spec/trash_spec.lua Normal file
View file

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

32
spec/url_spec.lua Normal file
View file

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

View file

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

65
spec/win_options_spec.lua Normal file
View file

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

Some files were not shown because too many files have changed in this diff Show more