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.
* 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)
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.
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.
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.
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.
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.
* 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
* 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>
* 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
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.
* 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
* 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
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
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.
* 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
* 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
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).
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`.
* 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
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.
* 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
* 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
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.
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.
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.
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.
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.
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.
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.