doc/fileops.txt — rendered from the plugin's own vimdoc
*fileops.txt* File operations for Neovim *fileops.nvim* Author: Stefan Bartl Version: 0.4.0
CONTENTS
1. Introduction .............. |fileops-intro| 2. Requirements .............. |fileops-requirements| 3. Installation .............. |fileops-installation| 4. Configuration ............. |fileops-config| 5. The :File command ......... |fileops-command| 5.1 Subcommands ............ |fileops-subcmds| 5.2 Navigation targets ...... |fileops-targets| 6. Keymaps ................... |fileops-keymaps| 7. Autocommands ............... |fileops-autocmds| 8. Which-key .................. |fileops-whichkey| 9. Lua API ................... |fileops-api| 10. Tab completion ............ |fileops-completion| 11. Health check .............. |fileops-health| 12. Architecture .............. |fileops-architecture|
1. INTRODUCTION
fileops.nvim provides a single unified:Filecommand for all common file operations: create, navigate, rename, duplicate, and delete — plus matching keymaps and a Lua API. All file I/O goes throughvim.uv(libuv) directly. No shell commands, no injection risk, fully cross-platform (Windows and Unix).
2. REQUIREMENTS
- Neovim 0.9 or later - lib.nvim (https://github.com/StefanBartl/lib.nvim) — REQUIRED. Supplies the :File command layer (lib.nvim.bindings.usercmd.composer), the injection-safe file primitives behind create/rename/duplicate/delete (lib.nvim.cross.fs.mutate), and background buffer opening (lib.nvim.buffer.open_background). Only notify's own styling is a genuinely soft, cosmetic fallback. - ui.nvim (https://github.com/StefanBartl/ui.nvim) — REQUIRED. ui.kit backs every prompt fileops has no other UI for — the missing-destination prompt (:File rename/move/duplicate/copy/touch/new with no argument), the modified-buffer confirm on :File next/:File prev, and :File bulk rename / the bulk-rename keymap. Lazily required, so nothing loads it until one of those runs, but there is no fallback: the default keymaps and several :File subcommands raise an error without it.
3. INSTALLATION
lazy.nvim:
{
"StefanBartl/fileops.nvim",
dependencies = { "StefanBartl/lib.nvim", "StefanBartl/ui.nvim" }, -- both required
event = "VeryLazy",
opts = {},
}
packer.nvim / pckr.nvim:
use({
"StefanBartl/fileops.nvim",
requires = { "StefanBartl/lib.nvim", "StefanBartl/ui.nvim" }, -- both required
config = function()
require("fileops").setup()
end,
})
4. CONFIGURATION
require("fileops").setup({
cycle = {
open_target = "replace",
keep_focus = true,
include_hidden = false,
wrap = true,
follow_symlinks = true,
root = "buffer_dir", -- or "cwd"|"buffer_dir_recursive"|"cwd_recursive"
confirm_on_modified = true,
case_insensitive = true,
pattern = nil,
},
cd = {
scope = "window",
refresh_explorers = true,
},
explorer = {
refresh_on_change = true,
},
delete = {
mode = "trash", -- "trash"|"permanent"
on_before_delete = nil, -- fun(path): boolean|nil
},
git_aware = {
enable = false,
warn_only = true,
git_cmd = "git",
},
integrations = {
ui_menu = true,
},
session_compat = {
enable = true,
},
keymaps = {
cycle = true,
delete = true,
lhs = {
next_replace = "<leader>nf",
prev_replace = "<leader>pf",
next_current = "<leader>nfn",
prev_current = "<leader>pfn",
next_background = "<leader>nF",
prev_background = "<leader>pF",
next_vsplit = "<leader>NF",
prev_vsplit = "<leader>PF",
delete = "<leader>dcf",
},
},
commands = true,
auto_mkdir = {
enable = true,
skip_remote = true,
detect_remote_pattern = "^%w%w+:[\\/][\\/]",
},
on_hold = {
enable = false, -- opt-in
modes = "n", -- "n"|"v"|"i" (any combination) or array; nil = n+v
delay = 3000, -- extra debounce (ms) beyond 'updatetime'
throttle_ms = 1200, -- min time (ms) between triggers per window
git_cmd = "git",
ignore_buftypes = { "nofile", "prompt", "terminal" },
only_tracked = true, -- skip files not tracked by git
require_clean_buffer = false, -- skip if buffer has unsaved changes
prefix = "previous: ", -- prefix before fallback EOL preview text
right_align = false, -- place virt_text right-aligned instead of eol
max_len = 160, -- truncate fallback preview to this many chars
hl_prev = "Comment",
virt_priority = 1000,
prefer_inline = true, -- prefer gitsigns.preview_hunk_inline()
restore_view = true, -- save/restore winsaveview()+cursor
events_override = nil, -- fully override auto-mapped events
},
conflict_marks = {
enable = true,
hl_a = "DiffDelete", -- "<<<<<<<" lines
hl_b = "DiffChange", -- "=======" separator
hl_c = "DiffAdd", -- ">>>>>>>" lines
},
gitsuite_events = {
enable = true, -- refresh explorers on gitsuite.nvim branch-switch/conflict-resolved events
},
})
cycle.open_target
"replace"|"current"|"split"|"vsplit"|"tab"|"background"— default open mode for:File next/:File prev. Default:"replace".
cycle.keep_focus
Return focus to origin window after split/vsplit. Default: true.
cycle.include_hidden
Include dot-files in directory listing. Default: false.
cycle.wrap
Wrap around at directory boundary. Default: true.
cycle.follow_symlinks
Useuv.fs_realpathfor path canonicalisation. Default:true.
cycle.root
"buffer_dir","cwd","buffer_dir_recursive"or"cwd_recursive"— directory to scan. The_recursivevariants also walk subdirectories (symlinked directories are never descended into, so a symlink cycle can't cause an infinite walk). Default:"buffer_dir".
cycle.confirm_on_modified
Show aui.kit.confirmdialog ("Save and open" / "Discard changes and open" / "Cancel") when the buffer has unsaved changes andopen_target = "replace". Requires ui.nvim — there is no fallback. Default:true.
cycle.case_insensitive
Sort and compare filenames case-insensitively. Default: true.
cycle.pattern
Glob filter (e.g."*.lua") applied to file names before navigating, matched viaglob2regpat(). Overridable per-call via |:File-next|'s[glob]argument. Default:nil(no filter).
cd.scope
"window"(:lcd),"tab"(:tcd) or"global"(:cd) — scope of the directory change for:File cd. Default:"window".
cd.refresh_explorers
After changing directory, refresh an open neo-tree, nvim-tree or netrw so it
tracks the new root. Default: true.
explorer.refresh_on_change
Reload neo-tree/nvim-tree in place (no root change) after any file op that changes the tree: new/write/saveas/writeto/mkdir/touch/rename/move/ duplicate/copy/delete. AUser FileopsChangedautocmd fires regardless of this setting. See |fileops-autocmds|. Default:true.
delete.mode
"trash"(OS trash/recycle bin via lib.nvim.fs.trash — Windows Recycle Bin, macOS Finder, Linuxgio trash/trash-put) or"permanent"(uv.fs_unlinkvia lib.nvim.cross.fs.mutate, no undo). Default:"trash".
delete.on_before_delete
fun(path: string): boolean|nilcalled right before a file is deleted (by |:File-delete| orfileops.delete_current()); returnfalseto abort (nothing is deleted, the buffer is untouched). Useful for a git-tracked-file warning. Default:nil.
git_aware.enable
Master switch for git-tracked-file awareness on rename/move/duplicate/copy/ delete. Opt-in — shells out togit ls-filesto check tracked-ness. Default:false.
git_aware.warn_only
true: just note tracked-ness in the op's result message, still use libuv/uv.fs_unlinkunderneath.false: usegit mvfor rename/move andgit rmfor delete instead (delete only applies this whendelete.mode == "permanent"— trashing a file is a different operation thangit rm). Default:true.
git_aware.git_cmd
Git executable to use. Default: "git".
retry.attempts
How many times |:File-rename|/|:File-move|/|:File-duplicate|/|:File-copy|/ |:File-delete| try a filesystem op that failed with a transient sharing violation (EBUSY/EPERM/EACCES), counting the first attempt. On Windows those codes usually mean another process has the file open for a moment (antivirus, search indexer, OneDrive, a directory watcher) and the op succeeds a few hundred milliseconds later; on other platforms they mean what they say, so retrying is pointless there. Default:6on Windows,1elsewhere.
retry.backoff_ms
Base delay between attempts, doubling each round (60/120/240/480/960ms), so the default budget spends at most ~1.9s before reporting the failure. Each attempt firesUser FileopsRetryfirst, letting a plugin release its own handle on the path — see |fileops-autocmds|. Default:60.
integrations.ui_menu
Let ui.nvim's right-click menu (ui.menu) compose the File fly-out.falsehides it there only;integrations/menu.lua'sitems()still serves any other host. Default:true.
session_compat.enable
After |:File-rename|/|:File-move|, resave the active:mksessionsession (v:this_session) so it doesn't keep pointing at the old path. No-op when no session is active. Other session managers (possession.nvim, sessions.nvim, ...) can hook theUser FileopsChangedautocmd instead — see |fileops-autocmds|. Default:true.
keymaps.cycle
Master switch: register the<leader>nf/<leader>pffamily. Default:true.
keymaps.delete
Master switch: register<leader>dcf. Default:true.
keymaps.lhs
Per-keylhsoverrides. Set an entry tofalseto disable just that one keymap, or to a different string to remap it. The master switches above still gate the whole family. See |fileops-keymaps|.
commands
Register:File. Default:true.
auto_mkdir.enable
Auto-create parent directories onBufWritePre(the automatic-on-save counterpart to |:File-mkdir|). Default:true. See |fileops-autocmds|.
auto_mkdir.skip_remote
Skip buffers whose name matchesauto_mkdir.detect_remote_pattern(e.g.ssh://,http://). Default:true.
auto_mkdir.detect_remote_pattern
Lua pattern used to detect remote buffer names.
Default: "^%w%w+:[\\/][\\/]".
on_hold.enable
Master switch for the ambient line-diff preview. Default: false
(opt-in). See |fileops-autocmds|.
on_hold.modes
Mode filter for the preview:"n","v","i"(any combination as a string, e.g."nv") or an array. Default:nil(Normal+Visual).
on_hold.events_override
If set, fully replaces the auto-mapped events, e.g.
{ "CursorHold", "CursorHoldI" }.
on_hold.delay
Extra debounce (ms) beyond'updatetime'before running. Default:3000.
on_hold.throttle_ms
Minimum time (ms) between triggers per window. Default: 1200.
on_hold.git_cmd
Git executable to use. Default: "git".
on_hold.ignore_buftypes
Skip these buftypes. Default: { "nofile", "prompt", "terminal" }.
on_hold.only_tracked
Skip files not tracked by git. Default: true.
on_hold.require_clean_buffer
Skip the preview if the buffer has unsaved changes. Default: false.
on_hold.prefix
Prefix before the fallback EOL preview text. Default: "previous: ".
on_hold.right_align
Place the fallback preview's virt_text right-aligned instead of at EOL.
Default: false.
on_hold.max_len
Truncate the fallback preview to this many characters. Default: 160.
on_hold.hl_prev
Highlight group for the fallback preview text. Default: "Comment".
on_hold.virt_priority
Extmark virt_text priority for the fallback preview. Default: 1000.
on_hold.prefer_inline
Prefergitsigns.preview_hunk_inline()when available. Default:true.
on_hold.restore_view
Save/restorewinsaveview()+ cursor around the inline preview to avoid scroll jumps. Default:true.
conflict_marks.enable
Master switch for conflict-marker highlighting. Default: true.
See |fileops-autocmds|.
conflict_marks.hl_a
Highlight group for<<<<<<<lines. Default:"DiffDelete".
conflict_marks.hl_b
Highlight group for=======separator lines. Default:"DiffChange".
conflict_marks.hl_c
Highlight group for>>>>>>>lines. Default:"DiffAdd".
gitsuite_events.enable
Refresh open explorers on gitsuite.nvim's `User GitsuiteBranchSwitched/GitsuiteConflictsResolved` events (GS-25). No dependency in either direction -- these are plainUserautocmds, so this is a no-op without gitsuite.nvim installed. Default:true.
5. THE :File COMMAND
:[count]File[!] {subcommand} [args…]
A single command that dispatches to all file operations.
[count] Applies to navigation: skip N files at once.
[!] Override safety checks (overwrite guards, confirm dialogs).
{subcommand} One of the subcommands listed below.
[args…] Subcommand-specific arguments (see below).
% as a scope argument explicitly refers to the current buffer's file;
it is always implied when omitted.
Every {path}/{dest} argument below is itself optional: omit it and the
command opens a ui.kit.input prompt instead of erroring (requires ui.nvim
— there is no fallback). Cancelling the prompt (<Esc> or an empty answer) is
a silent no-op.
Tab-completion for these arguments is relative to the current buffer's
directory, not Neovim's cwd — :File rename <Tab> browses files next to the
one you're editing. Input starting with ~, /, or a Windows drive letter
is left alone (treated as already absolute).
For |:File-rename|/|:File-move|/|:File-duplicate|/|:File-copy|, a typed
relative destination is resolved the same way: against the current file's
directory, so :File rename NEW.md renames in place instead of dropping the
file into Neovim's cwd. The create commands (|:File-new|, |:File-write|,
|:File-saveas|, |:File-writeto|, |:File-touch|) still resolve relative paths
against the cwd, like Vim's own |:write|.
|:File-rename|/|:File-move|/|:File-duplicate|/|:File-copy|/|:File-delete| are
git-aware when git_aware.enable = true (opt-in — see |fileops-config|): by
default they just note in the result message that the file is tracked; with
git_aware.warn_only = false, rename/move use git mv and delete uses
git rm instead of a plain filesystem op, keeping the git index in sync.
|:File-rename|/|:File-move| also resave the active :mksession session by
default (session_compat.enable, see |fileops-config|).
5.1 Subcommands
:File new [path] *:File-new*
Set the current buffer's file name to {path}. Creates parent directories
automatically. Does NOT write the buffer to disk.
Examples:
:File new lua/mymodule/init.lua
:File new ~/projects/foo/bar.lua
:File[!] write [path] *:File-write* Like |:File-new| but also writes the buffer to disk immediately.!overwrites an existing file (:write!). :File[!] saveas [path] *:File-saveas* Save the buffer under{path}(equivalent to:saveas). Buffer name changes to the new path. Creates parent directories.!overwrites. :File[!] writeto [path] *:File-writeto* Write a copy of the buffer to{path}without changing the buffer's name. Creates parent directories.!overwrites. :File mkdir *:File-mkdir* Create the parent directory hierarchy for the current buffer's file. :File touch [path] *:File-touch* Create an empty file at{path}if it doesn't already exist yet (creates parent directories). Realtouchsemantics: an existing file is left untouched, never truncated. Doesn't require or open a buffer.
:File touch notes/todo.md
:File[!] rename [%] [dest] *:File-rename* Rename (or move) the current file on disk to{dest}. Updates the buffer name and reloads the buffer from disk afterwards (resets signs/ diagnostics). Writes unsaved changes before renaming. Creates parent directories. An existing destination without!asks first (ui.kit.confirm) instead of just failing;!overwrites directly, no prompt.%is optional (current file is always the source):
:File rename newname.lua
:File rename % newname.lua → same
:File! rename ../other/file.lua → overwrite if exists, no prompt
:File[!] move [%] [dest] *:File-move* Move the current file on disk to{dest}(possibly a different directory) and update the buffer name — same underlying rename as |:File-rename|, but the buffer is NOT reloaded: content and undo history stay exactly as they were. Creates parent directories. An existing destination without!asks first instead of failing;!overwrites directly.
:File move ../elsewhere/file.lua
:File! move % ../elsewhere/file.lua
:File[!] duplicate [%] [dest] *:File-duplicate* Copy the current file to{dest}using libuv and open the copy. Creates parent directories. An existing destination without!asks first instead of failing;!overwrites directly.
:File duplicate backup.lua
:File! duplicate % backup.lua
:File[!] copy [%] [dest] *:File-copy* Copy the current file to{dest}using libuv, like |:File-duplicate|, but without opening the copy afterwards. Creates parent directories. An existing destination without!asks first instead of failing;!overwrites directly.
:File copy backup.lua
:File! copy % backup.lua
:File[!] delete [%] *:File-delete* Delete the current file from disk and close the buffer. Usesuv.fs_unlinkby default (no shell command), or the OS trash/recycle bin whendelete.mode = "trash"— see |fileops-config|. If the buffer has unsaved changes, plain:File deleteasks first (ui.kit.confirm) instead of just refusing — decline or leave it unanswered and nothing is deleted;!skips the prompt and deletes + force-closes directly. Ifdelete.on_before_deleteis set, it runs first and can abort the deletion by returningfalse.
:File delete
:File delete % → identical
:File! delete → delete + force-close a modified buffer, no prompt
:[count]File[!] next [target] [glob] *:File-next* Open the next file in the current directory. An optional trailing[glob](e.g.*.lua, matched viaglob2regpat()) narrows the listing before navigating. If the first argument isn't a recognized target keyword, it is treated as[glob]instead — so:File next *.luaworks without naming a target first.
:File next → next file, configured open_target
:File next vsplit → open in vertical split
:File next *.lua → next file matching *.lua
:File next vsplit *.lua → open in vertical split, matching *.lua
:2File next → skip 2 files
:File! next → bypass modified-buffer prompt
:[count]File[!] prev [target] [glob] *:File-prev* Open the previous file in the current directory. Same options as |:File-next|. :File[!] first [target] *:File-first* Jump straight to the first file in the current directory listing (alphabetical, respectingcycle.include_hidden/cycle.case_insensitive), instead of stepping one at a time with |:File-next|. Same[target]values and!behaviour as |:File-next|.
:File first
:File last vsplit
:File[!] last [target] *:File-last* Jump straight to the last file in the current directory listing. See |:File-first|. :File[!] open [target] *:File-open* Reopen the current buffer's own path in a different window target, without changing which file is shown. Same[target]values as |:File-next|;!skips the modified-buffer confirm the same way it does there.
:File open vsplit
:File open tab
:File! open
:File path [mode] *:File-path* Copy the current file's path to the unnamed register and the system clipboard (+).[mode]defaults toabs.abs(default) absolute pathrelrelative to cwdnamefile name onlydircontaining directory only Examples:
:File path
:File path rel
:File path name
:File info *:File-info*
Show size, last-modified time, and permissions for the current file,
via libuv fs_stat (cross-platform, including Windows).
:File info
:File lockinfo [path] *:File-lockinfo* Diagnose anEBUSY/EPERM/EACCESfailure: is the file locked right now, and which process holds it? Defaults to the current buffer's file. Run it right after a rename/move/delete failed.
:File lockinfo
The report names the path, tries a live rename probe, and lists the holding processes via the Windows Restart Manager (no administrator rights needed). It also goes to|:messages|, so it survives the notification timeout and can be copied into a bug report. Reading it: a foreign process in the holder list (antivirus, search indexer, OneDrive) is the cause and no retry can fix it;nvimitself means a handle leaked inside this Neovim, e.g. a file watcher that was never closed; an empty list with a failing probe means a kernel-level lock the Restart Manager doesn't track; and a successful probe means the lock was transient and has already passed — which is whatretry.attemptsexists for (see |fileops-config|). An open buffer is never the cause: Neovim closes a file after reading it and keeps only its swap file open. Holder lookup is Windows-only; the rename probe works everywhere. Implemented inlib.nvim.cross.fs.lockso other plugins report the same findings in the same words. :File[!] bulk rename {pattern} {replacement} *:File-bulk-rename* Batch-rename every regular file directly inside the current buffer's directory (no recursion) whose name changes undername:gsub(pattern, replacement)— a Lua pattern, not a glob, applied to the file name only. Files the pattern doesn't match, or thatgsubleaves unchanged, are left alone. Shows a preview of everyold -> newpair, then asks for confirmation via aui.kit.confirmdialog ("Rename N file(s)" / "Cancel"; requires ui.nvim — there is no fallback) before touching disk.!allows overwriting existing destinations; without it a conflicting destination is skipped and reported (other files in the batch still get renamed). Any open buffer pointing at a renamed file is re-pointed at the new path (no reload — same as |:File-move|), and each successful rename fires the usualUser FileopsChangedevent (see |fileops-autocmds|).
:File bulk rename ^draft_ final_
:File bulk rename %.txt$ .md
:File! bulk rename ^old_ new_
:File cd [scope] *:File-cd* Change the working directory to the directory of the current buffer's file, then refresh any open file explorer (neo-tree, nvim-tree, netrw) so it tracks the new root. Optional[scope]overridescd.scopefor this call:window:lcd— window-local (default)tab:tcd— tab-localglobal:cd— global Examples:
:File cd
:File cd global
:File help *:File-help* Show a short usage overview for every subcommand directly in the command line, without opening this helpfile.
5.2 Navigation targets
The[target]argument for |:File-next| and |:File-prev| overrides the configuredcycle.open_targetfor that single invocation.%orreplaceOpen in current window (may close old buffer)stayorcurrentEdit in-place, old buffer stays listedneworsplitHorizontal splitvsplitVertical splittabNew tab pagebgorbackgroundLoad into buffer list, don't switch focus
6. KEYMAPS
Registered only whensetup()is called, and only for keys whoselhsresolves to a string (see |fileops-config|).
Cycle keymaps (`keymaps.cycle = true`):
<leader>nf:File next(replace)<leader>pf:File prev(replace)<leader>nfn:File next(current)<leader>pfn:File prev(current)<leader>nF:File next(background)<leader>pF:File prev(background)<leader>NF:File next(vsplit)<leader>PF:File prev(vsplit) All cycle keymaps respectv:count1.
Delete keymap (`keymaps.delete = true`):
<leader>dcf:File deleteDisable or remap a single key without touching the rest of the family:
require("fileops").setup({
keymaps = {
lhs = {
next_replace = false, -- disable just <leader>nf
delete = "<leader>X", -- remap delete to <leader>X
},
},
})
7. AUTOCOMMANDS
auto_mkdir (`BufWritePre`)
Creates the parent directory hierarchy of the file about to be written — the automatic-on-save counterpart to |:File-mkdir|. Enabled by default. Configuration (see |fileops-config|):
require("fileops").setup({
auto_mkdir = {
enable = true,
skip_remote = true,
detect_remote_pattern = "^%w%w+:[\\/][\\/]",
},
})
<
auto_mkdir.enable ~
Master switch. Set to `false` to disable entirely. Default: `true`.
auto_mkdir.skip_remote ~
Skip buffers whose name looks like a remote/URL-style path (e.g.
`ssh://`, `http://`) so no local directory is created for them.
Default: `true`.
auto_mkdir.detect_remote_pattern ~
Lua pattern used to detect remote buffer names.
Default: `"^%w%w+:[\\/][\\/]"`.
on_hold (`CursorHold`/`CursorHoldI`)
Ambient, mode-aware line-diff preview. Prefers gitsigns'preview_hunk_inline()when available, otherwise falls back to showing the previous committed content of the current line as EOL/right-aligned virtual text (viagit blame/git show, argv-only — no shell). The blame half uses gitsuite.nvim'sfeatures.blame.for_locationinstead when it is installed (optional soft dep, GS-26) — reuses its parser rather than fileops' owngit blame --porcelain; thegit showhalf is always fileops' own either way. Per-window throttled (on_hold.throttle_ms), mode-aware (on_hold.modes), and cleared on the next cursor move. Setsvim.o.updatetime = 100when enabled. Disabled by default — opt-in. Enable with:
require("fileops").setup({ on_hold = { enable = true } })
<
See |fileops-config| for the full `on_hold.*` option list.
conflict_marks (`BufWinEnter`/`BufWinLeave`)
Highlights unresolved Git conflict markers (<<<<<<<,=======,>>>>>>>). Delegates to gitsuite.nvim's own parser/highlighter (features.conflict.refresh, optional soft dep, GS-26) when installed — exact marker-length matching, diff3/zdiff3 base sections, ambiguous-region handling, none of which the fixedmatchadd/matchdeletepatterns below know about. Falls back to those patterns, per-window, when gitsuite.nvim is absent. Enabled by default. Disable with:
require("fileops").setup({ conflict_marks = { enable = false } })
<
See |fileops-config| for the `conflict_marks.*` option list.
gitsuite_events (`User GitsuiteBranchSwitched`/`GitsuiteConflictsResolved`)
Refreshes open explorers via the same pathnotify_changeuses, reacting to gitsuite.nvim's post-action events (GS-25): a branch switch (actiongit-checkout,path= the repo root) or the last conflict marker in a buffer being resolved (actiongit-conflict-resolved,path= that buffer's file). No dependency in either direction — theseUserevents simply never fire without gitsuite.nvim installed. Enabled by default. Disable with:
require("fileops").setup({ gitsuite_events = { enable = false } })
<
See |fileops-config| for the `gitsuite_events.*` option list.
User FileopsChanged
Fired on every op that changes the file tree (new,write,saveas,writeto,mkdir,touch,rename,move,duplicate,copy,delete), so any plugin or user config can react — not just the two tree explorers fileops.nvim knows about directly.
vim.api.nvim_create_autocmd("User", {
pattern = "FileopsChanged",
callback = function(ev)
-- ev.data = { action = "rename"|"move"|..., path = "/abs/path" }
vim.notify(ev.data.action .. ": " .. ev.data.path)
end,
})
<
fileops.nvim itself also reloads neo-tree/nvim-tree in place after these
ops (no root change, unlike |:File-cd|), gated by
`explorer.refresh_on_change` (default `true`) — the event fires regardless
of that setting.
User FileopsRetry
Fired before each retry of a filesystem op that failed with a transient sharing violation (EBUSY/EPERM/EACCES— another process holding the file open; an open Neovim buffer is never the cause, Neovim closes the file after reading it and only keeps its swap file open).
vim.api.nvim_create_autocmd("User", {
pattern = "FileopsRetry",
callback = function(ev)
-- ev.data = { path = "/abs/path", attempt = 1, err = "EBUSY: …" }
-- Close your own handle on ev.data.path here.
end,
})
<
The wait between attempts runs the event loop, so a handle released in this
callback is actually gone by the next try. This matters because retrying is
useless against a handle held inside *this* Neovim: a file watcher that
never calls `handle:close()` still holds the file on the last attempt.
See `retry.*` in |fileops-config| for the budget.
8. WHICH-KEY
which-key.nvim (https://github.com/folke/which-key.nvim) is an OPTIONAL soft dependency. When installed, the<leader>nand<leader>pprefixes get the group labels "fileops: next file" and "fileops: prev file". Every key also carries its owndesc. The labels are thewhich_keyfield of the keymap spec inbindings/keymaps.lua.
9. LUA API
All functions are on the module table:require("fileops"). setup({opts}) *fileops.setup()* Configure and activate. Idempotent (subsequent calls are no-ops). next({opts}, {count}) *fileops.next()* Open the next file. {opts} overridescycleconfig for this call only. prev({opts}, {count}) *fileops.prev()* Open the previous file. first({opts}) *fileops.first()* Jump to the first file in the current directory listing. last({opts}) *fileops.last()* Jump to the last file in the current directory listing. open({opts}) *fileops.open()* Reopen the current buffer's own path in a different window target. copy_path({mode}) *fileops.copy_path()* Copy the current buffer's file path to the clipboard.{mode}: one of"abs"|"rel"|"name"|"dir", defaults to"abs". info() *fileops.info()* Show size/mtime/permissions for the current buffer's file. new_file({path}, {opts}) *fileops.new_file()* Set buffer name. {opts}:{ write?: boolean, bang?: boolean }. touch({path}) *fileops.touch()* Create an empty file if it doesn't already exist. No buffer required. rename({path}, {opts}) *fileops.rename()* Rename current file (reloads buffer). {opts}:{ bang?: boolean }. move({path}, {opts}) *fileops.move()* Move current file (no buffer reload). {opts}:{ bang?: boolean }. duplicate({path}, {opts}) *fileops.duplicate()* Copy to new path + open. {opts}:{ bang?: boolean, open?: boolean }. copy({path}, {opts}) *fileops.copy()* Copy to new path without opening it. {opts}:{ bang?: boolean }. delete_current({opts}) *fileops.delete_current()* Delete current file. {opts}: `{ force?: boolean, mode?: "trash"| "permanent", on_before_delete?: fun(path): boolean|nil }`. cd_here({opts}) *fileops.cd_here()* Change directory to the buffer's directory and refresh open explorers. {opts}:{ scope?: "lcd"|"cd"|"tcd", refresh?: boolean }.
10. TAB COMPLETION
:Fileprovides context-aware tab completion::File <Tab>→ lists all subcommands:File next <Tab>→ lists navigation targets (and accepts a glob):File cd <Tab>→ lists cd scopes (window/tab/global):File path <Tab>→ lists path modes (abs/rel/name/dir):File rename <Tab>→ lists "%" and file paths, relative to buffer dir:File new <Tab>→ file path completion, relative to buffer dir (all path-taking subcmds complete relative to the current buffer's directory, not cwd — see |fileops-command|)
11. HEALTH CHECK
:checkhealth fileops
Checks: - Neovim >= 0.9 -vim.uv/vim.loopavailable -ui.kit(ui.nvim) detected (required — backs every prompt: the missing-destination prompt, the modified-buffer confirm, bulk rename) -vim.fs.diravailable - Plugin loaded (guard flag set) -lib.nvimdetected (required — the:Filecommand layer) -lib.nvim.notifyin use (optional — cosmetic notification styling only) -which-keyintegration status (optional) -gitexecutable found (required for on_hold; used by git_aware when enabled) -gitsigns.nvimfound (optional; on_hold prefers its inline preview)
12. ARCHITECTURE
plugin/fileops.lua Load guard
lua/fileops/
init.lua Public API, setup()
config/init.lua Merge user opts over DEFAULTS, expose get()
config/DEFAULTS.lua Immutable default configuration
@types/init.lua LuaLS annotations
bindings/init.lua Orchestrates usrcmds + keymaps + autocmds
bindings/usrcmds.lua :File command with subcommand dispatch
bindings/keymaps.lua Per-key configurable keymap registrations
(which-key group labels are a field of the spec)
bindings/autocmds.lua auto_mkdir/on_hold/conflict_marks/gitsuite_events
registration
health.lua checkhealth provider
util/
notify.lua "[fileops] " notifier; soft lib.nvim.notify use
git.lua argv-only git helpers for the git_aware feature
excmd.lua file-taking Ex commands run on the path as given
ops/
cycle.lua directory listing, navigate, open_path, jump_edge
file.lua create/rename/move/duplicate/copy/delete/touch/info/…
bulk.lua bulk-rename plan + execute
features/
on_hold.lua ambient CursorHold line-diff preview
conflict_marks.lua conflict-marker highlighting
docs/BINDINGS.md Cheatsheet of every keymap/usrcmd/autocmd
TESTS/ Headless spec suite
Module load order: util → ops → config → bindings → init
lib.nvim is a required dependency: the :File command layer
(bindings/usrcmds.lua), ops/file.lua's lib.nvim.cross.fs.mutate, and
ops/cycle.lua's lib.nvim.buffer.open_background all require it
unconditionally. Only notifications are a genuinely soft, cosmetic
fallback: if lib.nvim.notify is present it is used for styling,
otherwise fileops.nvim falls back to plain vim.notify.
ui.nvim is likewise required: ui.kit is a bare, unguarded require at
every call site backing an interactive prompt fileops has no other UI for
(ops/cycle.lua's modified-buffer confirm, bindings/usrcmds.lua's
missing-destination prompt and bulk-rename confirm, bindings/keymaps.lua,
and integrations/filetree_assets.lua / integrations/menu.lua). There is
no vim.ui.select/vim.ui.input fallback; without ui.nvim those prompts
raise an error when triggered.