NORMAL ~/wkd/p/fileops/help :set skin=modern utf-8

fileops.txt

File operations for Neovim — fileops.nvim

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 *fileops-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-intro*

fileops.nvim provides a single unified :File command for all common file
operations: create, navigate, rename, duplicate, and delete — plus matching
keymaps and a Lua API.

All file I/O goes through vim.uv (libuv) directly. No shell commands, no
injection risk, fully cross-platform (Windows and Unix).

2. REQUIREMENTS *fileops-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 *fileops-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 *fileops-config*

  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

  Use uv.fs_realpath for path canonicalisation. Default: true.

cycle.root

  "buffer_dir", "cwd", "buffer_dir_recursive" or "cwd_recursive" —
  directory to scan. The _recursive variants 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 a ui.kit.confirm dialog ("Save and open" / "Discard changes and
  open" / "Cancel") when the buffer has unsaved changes and
  open_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 via glob2regpat(). 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. A User FileopsChanged autocmd 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, Linux gio trash/trash-put) or "permanent"
  (uv.fs_unlink via lib.nvim.cross.fs.mutate, no undo). Default: "trash".

delete.on_before_delete

  fun(path: string): boolean|nil called right before a file is deleted (by
  |:File-delete| or fileops.delete_current()); return false to 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 to git ls-files to 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_unlink underneath. false: use git mv for rename/move and
  git rm for delete instead (delete only applies this when
  delete.mode == "permanent" — trashing a file is a different operation
  than git 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: 6 on Windows, 1 elsewhere.

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 fires User FileopsRetry first, 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. false
  hides it there only; integrations/menu.lua's items() still serves any
  other host. Default: true.

session_compat.enable

  After |:File-rename|/|:File-move|, resave the active :mksession session
  (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 the User FileopsChanged autocmd instead —
  see |fileops-autocmds|. Default: true.

keymaps.cycle

  Master switch: register the <leader>nf / <leader>pf family.
  Default: true.

keymaps.delete

  Master switch: register <leader>dcf. Default: true.

keymaps.lhs

  Per-key lhs overrides. Set an entry to false to 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 on BufWritePre (the automatic-on-save
  counterpart to |:File-mkdir|). Default: true. See |fileops-autocmds|.

auto_mkdir.skip_remote

  Skip buffers whose name matches auto_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

  Prefer gitsigns.preview_hunk_inline() when available. Default: true.

on_hold.restore_view

  Save/restore winsaveview() + 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 plain User autocmds, so this
  is a no-op without gitsuite.nvim installed. Default: true.

5. THE :File COMMAND *fileops-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 *fileops-subcmds*

: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). Real touch semantics: 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. Uses uv.fs_unlink
  by default (no shell command), or the OS trash/recycle bin when
  delete.mode = "trash" — see |fileops-config|. If the buffer has unsaved
  changes, plain :File delete asks 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. If
  delete.on_before_delete is set, it runs first and can abort the
  deletion by returning false.
    :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 via glob2regpat()) narrows the listing before
  navigating. If the first argument isn't a recognized target keyword, it is
  treated as [glob] instead — so :File next *.lua works 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, respecting cycle.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 to abs.

    abs   (default)  absolute path
    rel              relative to cwd
    name             file name only
    dir              containing 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 an EBUSY/EPERM/EACCES failure: 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; nvim itself
  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 what retry.attempts
  exists 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 in lib.nvim.cross.fs.lock so
  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 under
  name:gsub(pattern, replacement) — a Lua pattern, not a glob, applied to
  the file name only. Files the pattern doesn't match, or that gsub leaves
  unchanged, are left alone.

  Shows a preview of every old -> new pair, then asks for confirmation via
  a ui.kit.confirm dialog ("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 usual
  User FileopsChanged event (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] overrides cd.scope for this call:

    window   :lcd  — window-local (default)
    tab      :tcd  — tab-local
    global   :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 *fileops-targets*

The [target] argument for |:File-next| and |:File-prev| overrides the
configured cycle.open_target for that single invocation.

  %  or  replace     Open in current window (may close old buffer)
  stay  or  current  Edit in-place, old buffer stays listed
  new  or  split     Horizontal split
  vsplit               Vertical split
  tab                  New tab page
  bg  or  background Load into buffer list, don't switch focus

6. KEYMAPS *fileops-keymaps*

Registered only when setup() is called, and only for keys whose lhs
resolves 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 respect v:count1.

Delete keymap (`keymaps.delete = true`):

  <leader>dcf  :File delete

Disable 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 *fileops-autocmds*

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 (via git blame/git show, argv-only — no shell). The blame half
  uses gitsuite.nvim's features.blame.for_location instead when it is
  installed (optional soft dep, GS-26) — reuses its parser rather than
  fileops' own git blame --porcelain; the git show half 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. Sets
  vim.o.updatetime = 100 when 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 fixed matchadd/matchdelete patterns 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 path notify_change uses, reacting
  to gitsuite.nvim's post-action events (GS-25): a branch switch (action
  git-checkout, path = the repo root) or the last conflict marker in a
  buffer being resolved (action git-conflict-resolved, path = that
  buffer's file). No dependency in either direction — these User events
  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 *fileops-whichkey*

which-key.nvim (https://github.com/folke/which-key.nvim) is an OPTIONAL soft
dependency. When installed, the <leader>n and <leader>p prefixes get the
group labels "fileops: next file" and "fileops: prev file". Every key also
carries its own desc. The labels are the which_key field of the keymap
spec in bindings/keymaps.lua.

9. LUA API *fileops-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} overrides cycle config 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 *fileops-completion*

:File provides 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 *fileops-health*

  :checkhealth fileops
Checks:
  - Neovim >= 0.9
  - vim.uv / vim.loop available
  - ui.kit (ui.nvim) detected (required — backs every prompt: the
    missing-destination prompt, the modified-buffer confirm, bulk rename)
  - vim.fs.dir available
  - Plugin loaded (guard flag set)
  - lib.nvim detected (required — the :File command layer)
  - lib.nvim.notify in use (optional — cosmetic notification styling only)
  - which-key integration status (optional)
  - git executable found (required for on_hold; used by git_aware when enabled)
  - gitsigns.nvim found (optional; on_hold prefers its inline preview)

12. ARCHITECTURE *fileops-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.

keys

j / k
next / previous line
gg / G
first / last line
⏎
open the line under the cursor
/
search the plugins
:
command line — Tab completes
:help x
vimdoc of a plugin (:e x = plugin page)
:ls · :log · :stack
plugin list · activity stream · dependency graph
:colo x
colorscheme
:set skin=
modern | tui
?
this help
esc
close