Skip to content

Search and replace in the current buffer with incremental preview, a convenient UI, and modern regex syntax.

License

Notifications You must be signed in to change notification settings

chrisgrieser/nvim-rip-substitute

Repository files navigation

rip-substitute 🪦

badge

Search and replace in the current buffer with incremental preview, a convenient UI, and modern regex syntax.

showcase.mp4

Table of Contents

Features

  • Search and replace in the current buffer using ripgrep.
  • Uses common regex syntax — no more dealing with arcane vim regex.
  • Incremental preview of matched strings and replacements, live count of matches.
  • Popup window instead of command line. This entails:
    • Syntax highlighting of the regex.
    • Editing with vim motions.
    • No more dealing with delimiters.
  • Sensible defaults: entire buffer (%), all matches in a line (/g), case-sensitive (/I).
  • Substitute only in a range, with visual emphasis of the range
  • History of previous substitutions.
  • Performant: In a file with 5000 lines and thousands of matches, still performs blazingly fast.™
  • Regex101 integration: Open the planned substitution in a pre-configured regex101 browser-tab for debugging.
  • Quality-of-Life features: automatic prefill of the escaped cursorword, adaptive popup window width, toggle --fixed-strings, …
  • Syntax comparison:
    # all three are equivalent
    
    # vim's :substitute
    :% s/\(foo\)bar\(\.\)\@!/\1baz/gI
    
    # vim's :substitute (very magic mode)
    :% s/\v(foo)bar(\.)@!/\1baz/gI
    
    # rip-substitute
    (foo)bar(?!\.)
    $1baz

Installation

Requirements

  • ripgrep with pcre2 support
    • brew install ripgrep (already includes pcre2 by default)
    • cargo install ripgrep --features pcre2
  • Alternatively, you can also use this plugin without pcre2 by setting regexOptions.pcre2 = false in the config. However, some features like lookaheads are not supported then.
  • Nvim >= 0.10
  • optional: :TSInstall regex (adds syntax highlighting)
-- lazy.nvim
{
	"chrisgrieser/nvim-rip-substitute",
	cmd = "RipSubstitute",
	opts = {},
	keys = {
		{
			"<leader>fs",
			function() require("rip-substitute").sub() end,
			mode = { "n", "x" },
			desc = " rip substitute",
		},
	},
},

-- packer
use {
	"chrisgrieser/nvim-rip-substitute",
	config = function() 
		require("rip-substitute").setup()
	end,
}

Configuration

-- default settings
require("rip-substitute").setup {
	popupWin = {
		title = " rip-substitute",
		border = "single",
		matchCountHlGroup = "Keyword",
		noMatchHlGroup = "ErrorMsg",
		hideSearchReplaceLabels = false,
		---@type "top"|"bottom"
		position = "bottom",
	},
	prefill = {
		---@type "cursorWord"| false
		normal = "cursorWord",
		---@type "selectionFirstLine"| false (does not work with ex-command see README)
		visual = "selectionFirstLine",
		startInReplaceLineIfPrefill = false,
		alsoPrefillReplaceLine = false,
	},
	keymaps = { -- normal & visual mode, if not stated otherwise
		abort = "q",
		confirm = "<CR>",
		insertModeConfirm = "<C-CR>",
		prevSubst = "<Up>",
		nextSubst = "<Down>",
		toggleFixedStrings = "<C-f>", -- ripgrep's `--fixed-strings`
		toggleIgnoreCase = "<C-c>", -- ripgrep's `--ignore-case`
		openAtRegex101 = "R",
	},
	incrementalPreview = {
		matchHlGroup = "IncSearch",
		rangeBackdrop = {
			enabled = true,
			blend = 50, -- between 0 and 100
		},
	},
	regexOptions = {
		startWithFixedStringsOn = false,
		startWithIgnoreCase = false,
		-- pcre2 enables lookarounds and backreferences, but performs slower
		pcre2 = true,
		-- disable if you use named capture groups (see README for details)
		autoBraceSimpleCaptureGroups = true,
	},
	editingBehavior = {
		-- When typing `()` in the `search` line, automatically adds `$n` to the
		-- `replace` line.
		autoCaptureGroups = false,
	},
	notification = {
		onSuccess = true,
		icon = "",
	}
}

Note

Any ripgrep config file set via RIPGREP_CONFIG_PATH is ignored by this plugin.

Usage

lua function

vim.keymap.set(
	{ "n", "x" },
	"<leader>fs",
	function() require("rip-substitute").sub() end,
	{ desc = " rip substitute" }
)
  • Normal mode: prefills the escaped word under the cursor
  • Visual mode: prefills the escaped selection
  • Visual line mode: replacements are only applied to the selected lines (= the selection is used as range)

Ex-command
Alternatively, you can use the ex command :RipSubstitute, which also accepts a range argument. Note that when using the ex-command, visual mode and visual line mode both pass a range. To prefill the current selection, you therefore need to use the lua function.

" Substitute in entire file. Prefills the *escaped* word under the cursor.
:RipSubstitute

" Substitute in line range of the visual selection.
:'<,'>RipSubstitute

" Substitute in given range (in this case: current line to end of file).
:.,$ RipSubstitute

You can also pass a prefill for the search value, in which case the prefill is not escaped.

:RipSubstitute prefilled_unescaped_string

Advanced

Remember prefill
The function require("rip-substitute").rememberCursorWord() can be used to save the word under the cursor for the next time rip-substitute is called. (This overrides any other prefill for that run.)

One use case for this is to set a prefill for when you intend to run substitute with a range, since calling rip-substitute in visual line is not able to pick up a prefill.

Filetype
The popup window uses the filetype rip-substitute. This can be useful, for instance, to disable auto-pairing plugins in the popup window.

autoBraceSimpleCaptureGroups
A gotcha of ripgrep's regex syntax is that it treats $1a as the named capture group "1a" and not as the first capture group followed by the letter "a." (See ripgrep's man page on --replace for details.)

If regexOptions.autoBraceSimpleCaptureGroups = true (the default), rip-substitute automatically changes $1a to ${1}a, to make writing the regex more intuitive. However, if you regularly use named capture groups, you may want to disable this setting.

Limitations

  • Searching/replacing for line breaks (\n or \r) is not supported. See issue #28.
  • This plugin only searches the current buffer. To search and replace in multiple files via ripgrep, use a plugin like grug-far.nvim.

About the developer

In my day job, I am a sociologist studying the social mechanisms underlying the digital economy. For my PhD project, I investigate the governance of the app economy and how software ecosystems manage the tension between innovation and compatibility. If you are interested in this subject, feel free to get in touch.

I also occasionally blog about vim: Nano Tips for Vim

Buy Me a Coffee at ko-fi.com

About

Search and replace in the current buffer with incremental preview, a convenient UI, and modern regex syntax.

Topics

Resources

License

Stars

Watchers

Forks

Languages