replacer.nvim · Editing · vimdoc
:help replacer
Project-wide search and replace with interactive picker
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
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 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
• 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
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
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
*: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 fromrg --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
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.messagesandquietare omitted above since their defaults are{}/falserespectively 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 modefile_typesare 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.
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
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'slib.nvim.progressmodule — 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
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-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-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 printtrue. If it printsfalse, adddependencies = { "StefanBartl/lib.nvim" }to replacer's lazy.nvim spec.
11. 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
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