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

replacer.txt

Project-wide search and replace with interactive picker — replacer.nvim

doc/replacer.txt — rendered from the plugin's own vimdoc

*replacer.txt*  Project-wide search and replace with interactive picker
                                                               *replacer.nvim*

Author:  Stefan Bartl
Version: 0.5

CONTENTS *replacer-contents*

    1. Introduction ......................... |replacer-introduction|
    2. Requirements ......................... |replacer-requirements|
    3. Installation ......................... |replacer-installation|
    4. Quick Start .......................... |replacer-quickstart|
    5. Commands ............................. |replacer-commands|
    6. Configuration ........................ |replacer-configuration|
    7. Progress Indicator ................... |replacer-progress|
    8. Picker Mappings ...................... |replacer-mappings|
    9. Advanced Usage ....................... |replacer-advanced|
   10. Troubleshooting ...................... |replacer-troubleshooting|
   11. API .................................. |replacer-api|
   12. About ................................ |replacer-about|

1. INTRODUCTION *replacer-introduction*

Replacer provides project-wide search-and-replace with:

• Ripgrep-powered search with precise match coordinates
• Interactive selection via fzf-lua or Telescope
• Live context preview around each match
• UTF-8 multi-byte character support (umlauts, emoji, etc.)
• Replace only selected occurrences or all at once
• Bottom-up in-buffer edits to avoid offset shift bugs
• Literal mode by default; optional regex mode
• Multiple occurrences per line handled correctly
• |:Surround| — wrap every occurrence of a pattern with a delimiter

2. REQUIREMENTS *replacer-requirements*

• Neovim 0.9 or newer
• ripgrep (rg) in PATH: https://github.com/BurntSushi/ripgrep
  Recommended, not required — without it the native vimgrep backend is
  used automatically (no .gitignore awareness, no rich --type filtering).
• ONE of the following pickers:
  - telescope.nvim (+ plenary.nvim): https://github.com/nvim-telescope/telescope.nvim
  - fzf-lua: https://github.com/ibhagwan/fzf-lua
• lib.nvim (https://github.com/StefanBartl/lib.nvim) — REQUIRED: the
  :Replace/:Surround command layer (lib.nvim.bindings.usercmd.composer), notifications,
  the confirm dialog, and file export all depend on it directly. The
  progress indicator specifically (see |replacer-progress|) stays optional
  on top of that — without lib.nvim, searches simply run without any
  indicator.

3. INSTALLATION *replacer-installation*

With lazy.nvim:
    {
      "StefanBartl/replacer.nvim",
      name = "replacer.nvim",
      main = "replacer",
      dependencies = { "StefanBartl/lib.nvim" }, -- required
      opts = {
        engine = "telescope",  -- or "fzf"
        write_changes = true,
        confirm_all = true,
      },
    }
With packer.nvim:
    use {
      "StefanBartl/replacer.nvim",
      config = function()
        require("replacer").setup({
          engine = "telescope",
        })
      end,
    }

4. QUICK START *replacer-quickstart*

Basic usage:
    :Replace old new %           " current buffer
    :Replace old new cwd         " current directory
    :Replace old new /path       " specific path
    :Replace old new cwd All     " replace all without picker
With quotes (spaces, special chars):
    :Replace "foo bar" "baz qux" %
    :Replace \"test\" ok %
In picker (Telescope/fzf-lua):
    <CR>      Apply to selected entry/entries
    <Tab>     Toggle selection (multi-select)
    <C-a>     Replace all matches at once

Keys other than <CR> are configurable via keymaps in setup(), see
|replacer-mappings| and docs/BINDINGS.md.

5. COMMANDS *replacer-commands*

                                                                   *:Replace*
:[range]Replace[!] {old} {new} [scope] [--flags]
    Search for {old} and replace with {new}.

Parameters:

        {old}    Pattern to search (literal or regex, see config/--regex)
        {new}    Replacement text; empty string ("") deletes matches
        [scope]  Optional scope (default: config default_scope, "%"):
                   %       - current buffer (file-backed)
                   buf     - alias for %
                   cwd     - current working directory
                   .       - alias for cwd
                   root    - auto-detected project root, see |:ReplaceRoot|
                             and |replacer-root-detection|
                   <path>  - explicit file or directory
        [range]  A line range (e.g. :'<,'>Replace) restricts matching to the
                 selected lines of the current buffer.
        !        The bang form is shorthand for --all (non-interactive).

    Multiple occurrences on the same line each become a separate, selectable
    entry in the picker (and are all replaced under --all).

Flags (may appear anywhere; a lone "--" stops flag parsing):

        --literal | --no-literal | --regex   toggle literal vs regex search
        --smart-case | --no-smart-case       toggle ripgrep smart-case
        --hidden | --no-hidden               include/exclude dotfiles
        --ignore | --no-ignore               respect/ignore .gitignore
        --preserve-ws | --no-preserve-ws     keep a match's own leading/trailing
                                             whitespace around the replacement
        --case-preserve | --no-case-preserve re-case the replacement to match
                                             each match's own case style
        --word | --no-word                   whole-word matches only
        --code-only | --no-code-only         skip matches inside strings/
                                             comments (Tree-sitter, best-effort)
        --safe | --no-safe                   safe-mode: skip read-only/
                                             oversized/binary files
        --max-filesize=<bytes>                override the safe-mode size
                                             threshold for this run
        --to-quickfix                        send matches to the quickfix
                                             list and open it (never writes)
        --to-loclist                         send matches to the location
                                             list and open it (never writes)
        --changed | --changed=<kinds>        restrict to git changed files;
                                             bare = modified+staged+untracked,
                                             or a comma-list subset (e.g.
                                             --changed=modified,staged)
        --confirm-per-file | --no-confirm-per-file
                                             ALL-mode: ask All/Skip/Only-some/
                                             Quit per file (supersedes
                                             confirm_all/confirm_wide_scope)
        --also-rename-file | --no-also-rename-file
                                             single-file scope only: after a
                                             successful content replace, offer
                                             to also rename the file itself
        --lsp | --no-lsp                     identifier-shaped matches try an
                                             LSP-driven rename first, falling
                                             back to plain text
        --stream | --no-stream               incremental ripgrep --json
                                             parsing for smoother progress
                                             (see |replacer-streaming|)
        --checkpoint | --no-checkpoint       ALL-mode: snapshot touched files
                                             first; |:ReplaceUndo| restores them
        --type=<ft>      (repeatable)        restrict to a filetype (ripgrep)
        --glob=<pat>     (repeatable)        include glob pattern
        --exclude=<pat>  (repeatable)        exclude path/glob pattern
        --engine=<fzf|telescope>             override picker for this run
        --context=<n>                        preview context lines
        --all                                non-interactive: apply to every match
        --dry                                plan only: show stats + diff, no writes
        --export=<path>                      write the planned diff (or .json) to a file
                                             (implies --dry; .json -> JSON, else patch)

Completion:

                                              *replacer-completion*
        <Tab> completes the scope, every flag name (type "--" and press
        <Tab> for the full list), and the values of the four flags that
        have one:

            --type=       ripgrep's own type names, read live from
                          rg --type-list -- so a type added by your own
                          --type-add is offered too
            --changed=    modified|staged|untracked, comma-joinable; after
                          a comma only the kinds not yet named are offered
            --engine=     fzf|telescope
            --export=     file paths

        --glob= and --exclude= deliberately do NOT complete: they take
        patterns, not paths, and offering an existing file would suggest
        a candidate that is accepted but matches only that one file.

Examples:

        :Replace foo bar %
        :Replace "test.*" new cwd --regex
        :Replace old new . --all
        :Replace TODO DONE cwd --type=lua --exclude=node_modules
        :'<,'>Replace foo bar
        :Replace foo bar cwd --dry
        :Replace foo bar cwd --export=changes.patch
        :Replace foo bar cwd --export=plan.json

                                                                 *:Replacer*
:Replacer {args}
    Alias for |:Replace|

                                                               *:ReplaceRoot*
:ReplaceRoot[!] {old} {new} [--flags]
    Like |:Replace|, but the scope is always an auto-detected project root
    instead of a positional argument (see |replacer-root-detection|).
    Prompts with |vim.ui.select()| when detection finds more than one
    candidate directory. The bang form is shorthand for --all.

                                                               *:ReplaceUndo*
:ReplaceUndo [id]
    Restore every file from a --checkpoint snapshot: a byte-exact write
    back to disk, plus a reload of any currently loaded buffer for that
    path. Uses the most recent checkpoint when [id] is omitted; <Tab>
    completes known ids. See |replacer-checkpoint|.

                                                             *:ReplaceHistory*
:ReplaceHistory
    Open |vim.ui.select()| over the last 50 real applies (never dry-run/
    export/quickfix, which don't change anything). Picking an entry re-runs
    it via the interactive picker, unless the original run was --all. See
    lua/replacer/history.lua.

                                                         *:ReplaceSavePreset*
:ReplaceSavePreset {name} {old} {new} [scope] [--flags]
    Save a named, reusable replace request — including any flags/filters
    given — under {name}, overwriting any existing preset of that name.

                                                             *:ReplacePreset*
:ReplacePreset {name}
    Run a saved preset exactly as it was saved. <Tab> completes known
    preset names. Both commands persist to
    stdpath("data")/replacer/{history,presets}.json. See
    lua/replacer/presets.lua.

                                                              *:ReplaceBatch*
:ReplaceBatch[!] {source} [scope] [--flags]
    Run multiple {old -> new} pairs in one invocation. Each pair is
    dispatched as its own full :Replace run (search + apply, sequentially,
    non-interactive — no picker), reusing the exact same pipeline (dry-run,
    filters, checkpoints, hooks, history, …) as a normal :Replace! would.
    The bang is implied/optional: batch always runs non-interactively.
    With 'confirm_all' on and no --dry, the whole batch is confirmed ONCE
    up front, not once per pair; declining cancels the batch before any
    pair is dispatched.

    {source} is a file path, or one of:
        clipboard, +    the "+" register
        unnamed, "      the unnamed register
        qf, quickfix    the quickfix list's text fields

    Pairs, auto-detected:
        line format (default)  one "old => new" per line; "#" comments and
                                blank lines are ignored
        JSON                   a [{"old":"...","new":"..."}, ...] array,
                                used when the content starts with "["

Examples:

        :ReplaceBatch pairs.txt src/
        :ReplaceBatch clipboard cwd --dry
        :ReplaceBatch pairs.json .

                                                            *:ReplaceFNames*
:ReplaceFNames[!] {old} {new} [scope] [--dry]
    Rename every file/directory under [scope] whose basename contains
    {old} (literal substring) — renaming NAMES, not file contents.
    Renames are computed from one snapshot of the tree taken before
    anything is touched; when a match is nested inside another match, only
    the outer one is renamed this run (the inner one moves along for
    free) — re-run to catch it on its own if its name still matches
    afterward, the same idempotent shape as |:Surround|'s --nested
    handling. --dry opens a read-only preview split instead of renaming.
    The bang form applies non-interactively; otherwise a single confirm
    asks for the whole batch. Does NOT follow the rename through source
    references (imports/requires) across the project. See
    lua/replacer/fnames.lua and |replacer-rename-assist| for the narrower,
    single-file case.

                                                             *:ReplaceEscape*
:ReplaceEscape {text}
    Escape {text} for use as a Vim regex pattern (\, ., *, [, ], $, ^, ~,
    /). Echoes the escaped result and copies it to the unnamed register "
    for immediate pasting into a :Replace/:Surround invocation.

                                                               *:ReplaceTest*
:ReplaceTest [pattern] [sample]
    Open a small floating pattern-test panel: line 1 is the pattern, line 2
    the sample text. Every match of line 1 against line 2 is highlighted
    live as either line changes (hl_group "ReplacerTarget"). Close with
    <Esc> or q.

                                                                 *:Surround*
                                                                     *:Wrap*
:[range]Surround[!] {pattern} [delim] [scope] [--flags]
    Wrap every occurrence of {pattern} with a delimiter. This is a convenience
    layer over |:Replace|: it runs a literal replace where the replacement is
    <left>{pattern}<right>, so it inherits scope resolution, the picker, dry-run,
    --all, and every |replacer-commands| flag. :Wrap is an alias.

Parameters:

        {pattern}  Literal text to search for. Regex is NOT supported here (the
                   replacement is a fixed string, so each match must be equal);
                   search is always forced to literal mode.
        [delim]    The wrapper. One of:
                     • a literal char/string:  ` " ' * ** _  (used on both sides)
                     • a named alias (see below)
                     • a bracket opener ( [ { <  -> paired with its closer
                   When omitted, you are prompted for it.
        [scope]    Same as |:Replace|: % | buf · cwd | . · <file|dir>
                   (default: |replacer-config-default_scope|).
        [range]    Restricts matching to the selected buffer lines.
        !          Shorthand for --all (non-interactive, no picker).

Idempotency:

        By default, matches that are ALREADY wrapped by the chosen delimiter are
        skipped, so re-running :Surround never stacks extra layers. Running
        :Surround test ** on **test** leaves it untouched instead of
        producing ****test****. Pass --nested (alias --allow-nested) to opt in
        to another layer.

Surround-only flag:

        --nested   Also wrap matches already surrounded by this delimiter
                   (default: skip them). Alias: --allow-nested.

Delimiter aliases:

        b bt backtick tick code ........ `
        q dq quote quotes .............. "
        s sq single apos ............... '
        star asterisk .................. *
        bold strong .................... **
        italic underscore us ........... _
        paren parens round ............. ( )
        bracket brackets square ........ [ ]
        brace braces curly ............. { }
        angle angles ................... <

    Examples:~
        :Surround word `                 " `word`  in the current buffer
        :Surround word b                 " `word`  (alias for backtick)
        :Surround "foo bar" ** cwd       " **foo bar**  across the working dir
        :Surround TODO ( .               " (TODO)  project-wide, all files
        :Surround! name q %              " "name"  everywhere in buffer, no picker
        :'<,'>Surround item *            " *item*  within the selected lines
        :Surround word                   " prompt: "Surround with: "
        :Surround word ** --nested       " wrap even already-**bold** matches

6. CONFIGURATION *replacer-configuration*

Default configuration:
    require("replacer").setup({
      -- Picker UI: "auto" picks fzf-lua if present, else telescope.
      engine = "auto",                   -- "auto" | "fzf" | "telescope"

      -- Search backend: "auto" picks ripgrep if present, else vimgrep (native).
      search_engine = "auto",            -- "auto" | "ripgrep" | "vimgrep"

      -- Progress indicator (requires lib.nvim; silently skipped otherwise).
      progress_style = "auto",           -- "auto"|"notify"|"statusline"|"fidget"|"float"|"kit"

      -- Behavior
      write_changes = true,              -- write buffers after replace
      confirm_all = true,                -- ask before replacing all
      confirm_wide_scope = false,        -- extra confirm for non-buffer ALL

      -- Search options
      hidden = true,                     -- include dotfiles
      git_ignore = true,                 -- respect .gitignore (ripgrep)
      exclude_git_dir = true,            -- skip .git/ explicitly
      literal = true,                    -- fixed-strings by default
      smart_case = true,                 -- ripgrep -S
      preserve_whitespace = false,       -- keep a match's own leading/trailing ws
      case_preserve = false,             -- re-case replacement to match each match
      word_boundary = false,             -- keep only whole-word matches
      code_only = false,                 -- skip matches in strings/comments (best-effort)
      quiet = false,                     -- suppress routine info notifications
      messages = {},                     -- string.format template overrides
      safe_mode = false,                 -- skip read-only/oversized/binary files
      max_file_size = 5 * 1024 * 1024,   -- bytes; only enforced when safe_mode is true
      skip_binary = true,                -- only enforced when safe_mode is true
      default_scope = "%",               -- scope when none is given

      -- Default filters (also overridable per-run via flags)
      file_types = {},                   -- e.g. { "lua", "md" } (ripgrep --type)
      globs = {},                        -- e.g. { "*.lua" } (include globs)
      exclude = {},                      -- e.g. { "node_modules", "*.min.js" }

      -- Preview
      preview_context = 3,               -- context lines in preview

      -- ALL-mode confirmation / safety
      confirm_per_file = false,          -- see |replacer-confirm-per-file|
      checkpoint = false,                -- see |replacer-checkpoint|

      -- Extensibility
      hooks = {},                        -- see |replacer-hooks|
      lsp = false,                       -- see |replacer-soft-lsp-integration|
      stream = false,                    -- see |replacer-streaming|
      deps_popup = true,                 -- see |replacer-config-deps_popup|

      -- Picker keymaps (buffer-local); see |replacer-mappings|
      keymaps = {},

      -- Picker-specific options
      fzf = {},                          -- extra fzf-lua options
      telescope = {},                    -- extra telescope options
    })
This example is not exhaustive of every default value (e.g. messages and
quiet are omitted above since their defaults are {}/false respectively
with nothing interesting to show inline) — see the per-option entries below
and the relevant |replacer-advanced| subsection for anything not spelled out
here.

The native "vimgrep" backend has no external dependency and is used as an
automatic fallback when ripgrep is not installed. In that mode file_types
are matched as plain extensions and .gitignore is not consulted.

                                                   *replacer-config-engine*

engine

    Type: string
    Default: "auto"
    Values: "auto" | "fzf" | "telescope"

    Picker UI to use. "auto" prefers fzf-lua when installed, else telescope.

                                            *replacer-config-search_engine*

search_engine

    Type: string
    Default: "auto"
    Values: "auto" | "ripgrep" | "vimgrep"

    Match backend. "auto" prefers ripgrep when installed, else the native
    vimgrep scanner.

                                            *replacer-config-progress_style*

progress_style

    Type: string
    Default: "auto"
    Values: "auto" | "notify" | "statusline" | "fidget" | "float" | "kit"

    Progress indicator style. Requires the optional lib.nvim dependency;
    silently has no effect if it isn't installed. See |replacer-progress|.

                                             *replacer-config-write_changes*

write_changes

    Type: boolean
    Default: true

    Automatically write modified buffers after applying replacements.
    Set to false to review changes before saving.

                                               *replacer-config-confirm_all*

confirm_all

    Type: boolean
    Default: true

    Ask for confirmation before replacing all matches at once.

                                                    *replacer-config-hidden*

hidden

    Type: boolean
    Default: true

    Include hidden files (dotfiles) in search.

                                                *replacer-config-git_ignore*

git_ignore

    Type: boolean
    Default: true

    Respect .gitignore files.

                                           *replacer-config-exclude_git_dir*

exclude_git_dir

    Type: boolean
    Default: true

    Explicitly exclude .git/ directory.

                                                   *replacer-config-literal*

literal

    Type: boolean
    Default: true

    Use fixed-strings search (ripgrep --fixed-strings).
    Set to false for regex mode.

                                                *replacer-config-smart_case*

smart_case

    Type: boolean
    Default: true

    Enable smart-case search (ripgrep -S).

                                        *replacer-config-preserve_whitespace*

preserve_whitespace

    Type: boolean
    Default: false

    When true, a match's own leading/trailing whitespace (relevant with
    regex patterns like \s*foo\s*) is preserved around the replacement
    instead of being clobbered by it: `:Replace "\s*foo\s*" bar % --regex
    --preserve-ws` turns "  foo  " into "  bar  ", not "bar".

                                            *replacer-config-case_preserve*

case_preserve

    Type: boolean
    Default: false

    When true, the replacement is re-cased to match each match's own case
    style before being applied: foo->bar, Foo->Bar, FOO->BAR,
    fooBar->bazQux, FooBar->BazQux. See lua/replacer/casing.lua.

                                            *replacer-config-word_boundary*

word_boundary

    Type: boolean
    Default: false

    When true, only whole-word matches are kept: the byte immediately
    before and after the match must not be a letter/digit/underscore.

                                                *replacer-config-code_only*

code_only

    Type: boolean
    Default: false

    When true, matches falling inside a string/comment Tree-sitter node are
    skipped. Best-effort: fails open (keeps every match) for a file whose
    language has no available Tree-sitter parser, so this filter never
    silently drops matches it cannot classify. See lua/replacer/tscode.lua.

                                                *replacer-config-safe_mode*

safe_mode

    Type: boolean
    Default: false

    When true, read-only, oversized (see max_file_size), and binary (see
    skip_binary) files are skipped instead of touched. A skipped file is
    reported via a warning naming the reason.

                                            *replacer-config-max_file_size*

max_file_size

    Type: integer (bytes)
    Default: 5242880 (5 MiB)

    Only enforced when safe_mode is true. Also passed to ripgrep as
    --max-filesize so oversized files are skipped before they're even read.

                                              *replacer-config-skip_binary*

skip_binary

    Type: boolean
    Default: true

    Only enforced when safe_mode is true. Detected via a NUL-byte sniff of
    the file's first 512 bytes.

                                          *replacer-config-preview_context*

preview_context

    Type: integer
    Default: 3

    Number of context lines shown around match in preview.

                                            *replacer-config-default_scope*

default_scope

    Type: string
    Default: "%"

    Scope used when the command is invoked without one (e.g. :Replace foo bar).

                                                   *replacer-config-filters*

file_types~ globs~ exclude

    Type: string[]
    Default: {}

    Default filters applied to every search (also overridable per-run via the
    --type / --glob / --exclude flags, see |replacer-commands|):
        file_types  restrict to ripgrep --type values, e.g. { "lua", "md" }
        globs       include glob patterns, e.g. { "*.lua" }
        exclude     exclude path/glob patterns, e.g. { "node_modules" }

The following options are documented in full under |replacer-advanced| (each
tag below jumps straight to its subsection) rather than repeated here:

                                          *replacer-config-confirm_per_file*

confirm_per_file

    Type: boolean · Default: false · See |replacer-confirm-per-file|.

                                                *replacer-config-checkpoint*

checkpoint

    Type: boolean · Default: false · See |replacer-checkpoint|.

                                                     *replacer-config-hooks*

hooks

    Type: table · Default: {} · See |replacer-hooks|.

                                                       *replacer-config-lsp*

lsp

    Type: boolean · Default: false · See |replacer-soft-lsp-integration|.

                                                    *replacer-config-stream*

stream

    Type: boolean · Default: false · See |replacer-streaming|.

                                                 *replacer-config-keymaps*

keymaps

    Type: table · Default: see |replacer-mappings| · See |replacer-mappings|.

                                                     *replacer-config-quiet*

quiet

    Type: boolean · Default: false · See |replacer-i18n-messages|.

                                                  *replacer-config-messages*

messages

    Type: table · Default: {} · See |replacer-i18n-messages|.

                                              *replacer-config-deps_popup*

deps_popup

    Type: boolean
    Default: true

    Whether the one-time "which CLI tools does this plugin want, and why"
    popup shows on first setup() after install. Backed by lib.nvim's deps
    module (declared in docs/install.json); also repeatable via `:Lib deps
    show replacer.nvim` and folded into |:checkhealth| replacer. Disabling
    it here only affects this plugin — vim.g.lib_nvim_deps_disable_first_run
    / vim.g.lib_nvim_deps_disabled_plugins (docs/installation.md) disable it more
    broadly without touching any plugin's own config.

7. PROGRESS INDICATOR *replacer-progress*

On a large scope (cwd, a big directory) both phases can take a few seconds:
the search, and — for --all / :Replace! — the apply, which loads and
rewrites every matched file. Both report through lib.nvim's
lib.nvim.progress module — an OPTIONAL dependency (see
|replacer-installation|). Without it, everything works exactly as before,
just silently without any indicator.

The apply runs asynchronously once the match set spans more than ten files:
it is applied in chunks across event-loop ticks instead of one blocking
loop, so the editor stays responsive and the indicator shows "applying
replacements… N/total". A smaller set is applied synchronously, exactly as
before. Either way the "N file(s), M spot(s)" result notification is the
same.

A handle only becomes visible after ~150ms, so a fast search or a
single-buffer replace never flashes any UI, regardless of style.

                                                *replacer-progress-styles*

Styles (|replacer-config-progress_style|):

    "auto"        (default) prefers "fidget" if fidget.nvim is installed,
                  else "notify". Never picks "float"/"kit" on its own.
    "notify"      vim.notify; updated in place if the active backend
                  supports it (e.g. nvim-notify), else sequential notifies.
    "statusline"  draws nothing — read the live text from your own
                  statusline, see |replacer-progress-statusline| below.
    "fidget"      renders through fidget.nvim's LSP-style progress corner
                  (requires fidget.nvim; falls back to "notify" if missing
                  when requested explicitly).
    "float"       small floating window, bottom-right, never steals focus.
                  Focus it on purpose and press <Esc> (normal mode) for a
                  confirm prompt: "Yes" aborts the running search, "No"
                  leaves it running. The <Esc> keymap is buffer-local to
                  that window, so nothing happens while any other window is
                  focused.
    "kit"         same interaction as "float", rendered through
                  ui.kit's themed surface instead of a fixed look
                  (matches whatever preset you configured for ui.kit).

                                             *replacer-progress-statusline*

Using the "statusline" style

This style draws nothing by design — it exists so you can fold the current
search status into your OWN statusline. Read it from
lib.nvim.progress.styles.statusline:

    local replacer_status = require("lib.nvim.progress.styles.statusline")

    local function my_statusline_component()
      local active = replacer_status.active()  -- string[], oldest first
      if #active == 0 then return "" end
      return table.concat(active, " | ")
    end
active() returns one entry per currently in-flight progress handle across
ALL plugins using this style (a shared, headless registry, not replacer-
specific), already prefixed with each handle's title (e.g. "[replacer] ").
It updates live and clears itself on finish/cancel — no polling needed, and
every change triggers :redrawstatus so it refreshes even while you're idle.

See docs/progress-indicator.md in the repository for copy-pasteable
lualine / vanilla-statusline snippets.

8. PICKER MAPPINGS *replacer-mappings*

All keys below except <CR> are configurable: `require("replacer").setup({
keymaps = { toggle_select=..., toggle_select_prev=..., apply_all=...,
replace_and_reopen=..., quit=... } })`. Defaults shown match the previous
hardcoded behavior exactly. which-key.nvim (if installed) shows labels for
these — see docs/BINDINGS.md for exactly which keys are visible to
which-key per backend.

                                              *replacer-mappings-telescope*

Telescope mappings:

    <CR>      Apply replacement to selected entry/entries (fixed)
    <Tab>     Toggle selection (multi-select mode)         [keymaps.toggle_select]
    <S-Tab>   Toggle selection, move backward              [keymaps.toggle_select_prev]
    <C-a>     Replace all matches at once (with confirmation) [keymaps.apply_all]
    <C-r>     Apply entry under cursor, reopen with the rest [keymaps.replace_and_reopen]
    <Esc>     1st: switch to normal mode · 2nd (in normal mode): close [keymaps.quit]

                                                  *replacer-mappings-fzf*

fzf-lua mappings:

    <CR>      Apply replacement to selected entries (fixed)
    <Tab>     Toggle selection                             [keymaps.toggle_select]
    <S-Tab>   Toggle selection, move backward               [keymaps.toggle_select_prev]
    <C-a>     Replace all matches at once (with confirmation) [keymaps.apply_all]
    <C-r>     Apply entry under cursor, reopen with the rest [keymaps.replace_and_reopen]
    <Esc>     1st: leave terminal-insert (fixed) · 2nd: close [keymaps.quit]

9. ADVANCED USAGE *replacer-advanced*

                                                    *replacer-utf8-handling*

UTF-8 Multi-Byte Characters

Replacer correctly handles UTF-8 multi-byte characters (umlauts, emoji, etc.)
by converting character offsets to byte offsets internally.

Example with German umlauts:
    :Replace "Müller" "Mueller" %
If a match is reported as "skipped (changed content)", the buffer differs from
what was searched — re-run :Replace to refresh the match coordinates.

                                                     *replacer-dry-run*

Dry-Run & Export

Preview the exact effect of a replacement without writing anything:
    :Replace foo bar cwd --dry
This reports a stats summary (spots / files) and opens a read-only diff split
showing every planned change.

Export the planned change to a file (implies --dry):
    :Replace foo bar cwd --export=changes.patch   " git-applyable unified diff
    :Replace foo bar cwd --export=plan.json        " machine-readable JSON
A ".json" target produces JSON; any other extension produces a unified diff
that can be applied later with git apply changes.patch.

                                                *replacer-quickfix-export*

Quickfix / Location List Export

Send the match list to the quickfix or location list instead of applying —
never writes:
    :Replace foo bar cwd --to-quickfix   " :copen shows every match
    :Replace foo bar cwd --to-loclist    " :lopen, current-window loclist
Useful for a :cfdo %s/old/new/g | update-style workflow, or just to review
matches in a familiar list UI instead of the picker.

                                              *replacer-changed-files-only*

Changed-Files-Only Mode

Restrict matching to git changed/staged/untracked files, intersected with
the resolved scope (a specific dir/file still narrows the git file list, it
never widens it):
    :Replace foo bar cwd --changed          " modified + staged + untracked
    :Replace foo bar cwd --changed=staged   " staged files only
    :Replace foo bar cwd --changed=modified,untracked
Requires the scope to be inside a git repository; otherwise a warning is
shown and nothing runs. See lua/replacer/gitfiles.lua.

                                        *replacer-confirm-per-file*

Per-File Confirmation

--confirm-per-file (or config.confirm_per_file) replaces the single global
"Apply ALL N spot(s) across M file(s)?" confirmation with one confirmation
prompt per file, in --all mode:
    :Replace! foo bar cwd --confirm-per-file
Per file: All applies every match in that file immediately; Skip moves on
without touching it; Only some opens the interactive picker scoped to just
that file's matches AND STOPS THE LOOP THERE (like Quit) instead of also
prompting for the remaining files, since a second confirm float on top of
the still-open picker would corrupt it; re-run with --confirm-per-file to
continue with the rest. Quit (or <Esc>) stops the whole loop, leaving any
later files untouched. Supersedes confirm_all/confirm_wide_scope when
enabled. See lua/replacer/perfile.lua.

                                                     *replacer-checkpoint*

Undo Checkpoint

--checkpoint (or config.checkpoint) snapshots every file about to be
touched by an --all apply BEFORE any edit happens, so it can be undone:
    :Replace! foo bar cwd --checkpoint
    :ReplaceUndo
Snapshots live under stdpath("data")/replacer/checkpoints/<id>/ (one
directory per run, id is a sortable timestamp) as byte-exact copies — a
loaded, modified-but-unsaved buffer is snapshotted from its buffer content,
otherwise from disk. This is a plain file snapshot, NOT a git stash or temp
branch: a stash would also scoop up unrelated uncommitted work-in-progress
elsewhere in the same repository, which this plugin has no business doing.
:ReplaceUndo restores the most recent checkpoint (or an explicit [id],
<Tab>-completable) as a byte-exact write-back, reloading any buffer that
has that file open. See lua/replacer/checkpoint.lua.

                                                         *replacer-hooks*

Hooks

Lua before/after callbacks around the apply pipeline — run a linter/
formatter, invalidate a cache, log every change, etc. Two ways to
register:
    require("replacer").setup({
      hooks = {
        before_apply = function(ctx) -- { path, matches, new_text }
          if ctx.path:match("%.generated%.lua$") then return false end -- veto
        end,
        after_write = function(ctx) -- { path, bufnr, ok }
          if ctx.ok then vim.system({ "stylua", ctx.path }) end
        end,
      },
    })

    -- or programmatically, in addition to config.hooks:
    require("replacer.hooks").on("after_apply", function(ctx) -- { path, spots, skipped }
      print(string.format("%s: %d spot(s)", ctx.path, ctx.spots))
    end)
Events: before_apply (may return false to skip/veto that file), after_apply,
before_write, after_write — all fire once per file. A hook error is caught
and warned via notify.warn(), never aborts the apply. See
lua/replacer/hooks.lua.

                                                *replacer-rename-assist*

Rename-Assist

--also-rename-file pairs a single-file content replace with an offer to
also rename the file itself the same way (e.g. a class and its file in one
go). Scoped to single-file scope only (%/buf or an explicit file path —
never a directory tree; see |:ReplaceFNames| for that). A no-op (no prompt
at all) when the file's own basename doesn't contain {old} as a literal
substring — independent of --case-preserve, which only affects replaced
content, not the filename match:
    :Replace MyWidget MyButton % --also-rename-file --all
If the file is named e.g. MyWidget.lua, you're asked "Also rename
MyWidget.lua -> MyButton.lua?". See lua/replacer/rename_assist.lua.

                                             *replacer-soft-lsp-integration*

Soft LSP Integration

--lsp tries an LSP-driven rename (textDocument/rename, a proper
workspace-wide symbol rename) for each match whose old AND new text both
look like a plain identifier (letters/digits/underscore only) and whose
buffer has an attached LSP client that supports rename — always falling
back to the normal plain-text replace otherwise (no client, non-identifier
text, or the request fails/times out):
    :Replace MyWidget MyButton cwd --lsp --all
Position-based (uses each match's own line/column), not cursor-based, so
it never moves your cursor or window even when triggered from a picker.
Genuinely best-effort: mixed results (some matches LSP-renamed, others
plain-text) are normal and expected in one run. See
lua/replacer/lsp_rename.lua.

                                                       *replacer-streaming*

Streaming Collection

--stream switches ripgrep collection to an incremental --json parser
(rg.collect_streaming) instead of parsing the whole output at the end,
giving smoother, filter-aware progress updates while a large search is
still running.

Scope note: the picker itself still only opens once collection finishes —
true live picker fill (select matches while ripgrep is still running) is
not implemented yet. This flag ships the collection-layer infrastructure
for it (proven equivalent to the non-streaming collector by test); wiring
it into the pickers themselves is a follow-up, deliberately deferred given
the integration risk of terminal-UI live-population code that can't be
verified by an automated test suite. See lua/replacer/rg.lua's
collect_streaming docstring.

                                                   *replacer-i18n-messages*

i18n / Messages

Override any message template via config.messages (each a string.format
template; a malformed override is shown verbatim rather than erroring),
and/or suppress routine info-level notifications entirely with
config.quiet = true (warnings/errors always show):
    require("replacer").setup({
      quiet = false,
      messages = {
        no_matches = "keine Treffer",
        result = "%d Fundstelle(n) in %d Datei(en)",
      },
    })

Key Default Args

    confirm_all           "Apply ALL %d spot(s) across %d file(s)?"   spots, files
    confirm_all_short     "Apply replacement to ALL %d spot(s)?"      spots
    cancelled             "cancelled"                                -
    result                "%d spot(s) in %d file(s)"                 spots, files
    no_matches            "no matches found"                         -
    surround_prompt       "Surround with: "                          -
    surround_cancelled    "Surround: cancelled (no delimiter)"       -

See lua/replacer/messages.lua.

                                                *replacer-root-detection*

Monorepo / Project-Root Detection

The "root" scope token walks up from the current buffer's directory (or
cwd) looking for markers (.git, package.json, go.mod, Cargo.toml,
pyproject.toml, …). With several candidates (e.g. a monorepo package with
its own package.json nested inside a git repo), it deterministically
prefers the outermost one with .git, without prompting -- except the home
directory itself, which is never picked just because it happens to hold a
dotfiles .git; the nearest marker match wins instead:
    :Replace old new root
For an interactive prompt when there is more than one candidate, use
|:ReplaceRoot| instead:
    :ReplaceRoot old new --dry
See lua/replacer/root.lua.

                                                          *replacer-scopes*

Scope Examples

Current buffer only:
    :Replace old new %
Current working directory:
    :Replace old new cwd
    :Replace old new .
Specific directory:
    :Replace old new src/
    :Replace old new /absolute/path/
Specific file:
    :Replace old new src/main.lua
                                                    *replacer-highlighting*

Preview Highlighting

The preview window highlights the matched span on the target line
(hl_group "ReplacerTarget", linked to "Search" by default). Override it with
your colorscheme if desired:
    vim.api.nvim_set_hl(0, "ReplacerTarget", { bg = "#FF7A29", fg = "#1e1e1e" })
                                                *replacer-regex-backrefs*

Regex Backreferences

In regex mode (--regex), {new} may reference \(...\) capture groups from
{old} using \0 (whole match) through \9:
    :Replace "\(\w\+\)=\(\w\+\)" "\2_\1" % --regex
turns "foo=bar" into "bar_foo". A reference beyond what the pattern actually
captured expands to "". See also |:ReplaceEscape| and |:ReplaceTest| for
building/testing the pattern itself.

                                                    *replacer-quoted-args*

Quoted Arguments

Use quotes for patterns/replacements with spaces:
    :Replace "foo bar" "baz qux" %
Escape quotes inside quotes:
    :Replace "\"test\"" ok %
    :Replace '"test"' ok %
Backslash escapes work outside quotes too:
    :Replace \"test\" ok %

10. TROUBLESHOOTING *replacer-troubleshooting*

                                              *replacer-troubleshooting-utf8*

Problem: matches reported as "skipped (changed content)"

The recorded match no longer sits at its byte offset (the buffer changed since
the search, or the file encoding is not utf-8).

Solution:
1. Re-run the :Replace command to refresh match coordinates.
2. Check buffer encoding:
    :set fileencoding?
   It should be "utf-8". If not:
    :set fileencoding=utf-8
    :w
                                             *replacer-encoding-awareness*

BOM / CRLF

The real apply always goes through a Neovim buffer, which already strips a
UTF-8 BOM and normalizes CRLF -> LF on its own ('bomb'/'fileformat'), so
applies are unaffected by encoding. The native vimgrep backend's raw file
reads and the --dry/--export plan's fallback read (used when no buffer is
loaded) replicate the same normalization, so match offsets and previewed
content stay consistent with what the buffer-backed apply will actually do.
See lua/replacer/encoding.lua.

                                      *replacer-troubleshooting-parse-errors*

Problem: "Replace: ..." parse error

:Replace/:Surround report specific, actionable errors instead of a generic
parse failure:
    - Missing/too many positional arguments name exactly what's missing or
      extra.
    - An unterminated quote (e.g. :Replace "foo bar) is reported explicitly —
      close every quote, or escape a literal quote with \" / \'.
    - A boolean flag given a value (e.g. --dry=1) is rejected with a message
      naming the flag; boolean flags take no value, use --dry on its own.
    - An unknown option names the exact token that was not recognized.

                                         *replacer-troubleshooting-no-matches*

Problem: No matches found

Check if ripgrep finds matches directly:
    rg --json -n --column "pattern" file.lua
If ripgrep is not installed, replacer uses the native vimgrep backend
automatically; force a backend with :Replace ... --engine is for the picker,
while search_engine in setup() selects ripgrep/vimgrep.

                                    *replacer-troubleshooting-partial-matches*

Problem: Only first match per line found

This should not happen: every occurrence on a line becomes its own entry. If it
does, please open an issue with a reproducing input.

                                       *replacer-troubleshooting-not-atomic*

Note: multi-file applies are not atomic

Each file is written independently. If a write fails partway through a
multi-file replace, earlier files are already changed. Use --dry / --export to
review first, and prefer version control so changes are easy to revert.

                                          *replacer-troubleshooting-not-saved*

Problem: Changes not saved

Check configuration:
    write_changes = true  -- must be true for auto-save
Or manually save:
    :w
                                     *replacer-troubleshooting-no-progress*

Problem: No progress indicator ever appears

lib.nvim is not installed, or the search finished in under ~150ms (the
delay-guard). Confirm the dependency loads:
    :lua print(pcall(require, "lib.nvim.progress"))
Should print true. If it prints false, add
dependencies = { "StefanBartl/lib.nvim" } to replacer's lazy.nvim spec.

11. API *replacer-api*

For plugin developers and advanced users.

                                                         *replacer.setup()*
replacer.setup({opts})
    Configure the plugin.

Parameters:

        {opts}  Configuration table (see |replacer-configuration|)

Returns:

        nil

                                                           *replacer.run()*
replacer.run({request})
    Execute a replace workflow programmatically — the same executor every
    :Replace* command dispatches into.

    {request} is a structured RP_Request (see lua/replacer/types/init.lua):
    a table with old, new, scope, all, dry, export, line_range,
    overrides and filters fields. overrides carries the per-run flag
    values; filters narrows which files are searched.

    The legacy positional form replacer.run({old}, {new}, {scope}, {all}) is
    still accepted and normalised into the above.

Returns:

        nil

    Example:
        require("replacer").run({
          old = "oldName", new = "newName", scope = "cwd",
          all = false, dry = true,
          overrides = { case_preserve = true },
          filters = { file_types = { "lua" }, globs = {}, exclude = {} },
        })
        require("replacer").run("old", "new", "cwd", false)  -- legacy form
    docs/api.md additionally documents require("replacer.config").resolve()
    and require("replacer.hooks").on()/clear().

                                                  *replacer.config.get()*
require("replacer.config").get()
    Returns a deep copy of the current effective configuration.

    Example:
        local cfg = require("replacer.config").get()
        print(vim.inspect(cfg))

12. ABOUT *replacer-about*

Project: https://github.com/StefanBartl/replacer.nvim
Issues:  https://github.com/StefanBartl/replacer.nvim/issues
Documentation index (installation, configuration, commands, features,
workflow, troubleshooting): docs/README.md

Maintainer: Stefan Bartl

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