Regex Replace and Search Plugin for Obsidian written in Vanilla.js

Regex Replace Plugin for Obsidian

Overview

Regex Replace is a plugin that allows you to run regex-based search and replace operations directly inside the editor.
It comes with a convenient modal dialog, live preview of matches, and a preset system to save, overwrite, and delete commonly used regex operations.

Written in vanilla JavaScript :blush:


Installation

  1. Download the plugin files (manifest.json and main.js).

  2. Copy them into a folder inside your Obsidian vault, for example:

    <your-vault>/.obsidian/plugins/regex-replace/
    

    The folder should contain:

    manifest.json
    main.js
    
  3. Restart or reload Obsidian.

  4. Go to Settings → Community Plugins and enable Regex Replace.


Usage

You can open the plugin in two ways:

  • Command Palette (Ctrl+P / Cmd+P) → Search for Regex Search & Replace
  • Right-click context menu inside the editor → Regex Search & Replace

This will open the Regex Search & Replace Modal.


Features

1. Regex Input Fields

  • Regex pattern – enter the search pattern (JavaScript regex syntax).
  • Replacement text – enter the replacement string ($1, $2 etc. for capturing groups).
  • Flags – e.g. g (global), i (case-insensitive), m (multiline).

2. Options

  • Use only selection – restrict replacement to the current text selection.
  • Replace all – toggle between replacing all matches or just the first one.

3. Live Preview

  • Shows the first 50 matches in a scrollable table.
  • Each row displays the full line before and after replacement.
  • Matches are highlighted inline for better context.

4. Execute

  • Applies the regex replacement to either the current selection or the entire document.
  • Updates the editor content immediately.

Presets

Presets let you save, reuse, and manage regex operations.

Saving a Preset

  1. Configure your regex pattern, replacement, and options.
  2. Click “Save as preset”.
  3. Enter a preset name.
  4. The preset will appear in the dropdown.
  • If a preset with the same name already exists, it will be overwritten.
  • You will see a notice: “Preset overwritten: NAME”.

Loading a Preset

  1. Select a preset from the dropdown.
  2. The input fields (pattern, replacement, flags, etc.) will automatically update.
  3. Preview and Execute now use these loaded values.

Deleting a Preset

  1. Select the preset in the dropdown.
  2. Click “Delete preset”.
  3. The preset will be removed permanently.

Examples

Example 1: Swap First and Last Names

Text

Wisniewski Frank
Adenau
Wisniewski Frank
Adenau

Search pattern

^(\w+) (\w+)$\n^(\w+)

Replacement text

$2, $1, 53518 $3

Result

Frank, Wisniewski, 53518 Adenau
Frank, Wisniewski, 53518 Adenau

Example 2: Replace All i With x

Text

This is a simple line
With multiple words

Search pattern

i

Replacement text

x

Result

Thxs xs a sxmple lxne
Wxth multxple words

Tips

  • Use ^ and $ for line anchors.

  • Use capturing groups ( … ) with $1, $2 in the replacement.

  • Combine flags:

    • g = global (all matches)
    • i = case-insensitive
    • m = multiline (anchors match at line breaks)

Known Limitations

  • Very large documents may slow down preview if many matches are found (preview is capped at 50 matches).
  • Regex syntax follows JavaScript’s RegExp engine – not all features from PCRE or other regex engines are supported.

Files

manifest.json

{
  "id": "regex-replace",
  "name": "Regex Replace",
  "version": "1.0.0",
  "minAppVersion": "0.15.0",
  "description": "Führt Regex-Suchen und Ersetzen im aktiven Editor aus.",
  "author": "Frank Wisniewski",
  "isDesktopOnly": false
}

main.js

const { Plugin, Modal, Setting, Notice } = require("obsidian");

module.exports = class RegexReplacePlugin extends Plugin {
	async onload() {
		this.settings = Object.assign(
			{ 
				pattern: "", replacement: "", flags: "gm", 
				onlySelection: false, replaceAll: true,
				presets: []   
			}, 
			await this.loadData()
		);

		this.addCommand({
			id: "regex-search-replace",
			name: "Regex Search & Replace",
			editorCallback: (editor) => this.openModal(editor),
		});

		this.registerEvent(
			this.app.workspace.on("editor-menu", (menu, editor) =>
				menu.addItem((item) =>
					item.setTitle("Regex Search & Replace").setIcon("search").onClick(() => this.openModal(editor))
				)
			)
		);
	}

	openModal(editor) {
		new RegexReplaceModal(
			this.app,
			editor,
			this.settings,
			async (pattern, replacement, flags, onlySelection, replaceAll) => {
				try {
					let regexFlags = (flags || "").toString();
					if (!replaceAll) regexFlags = regexFlags.replace("g", "");
					const regex = new RegExp(pattern, regexFlags);

					if (onlySelection) {
						const sel = editor.getSelection();
						if (!sel) return new Notice("No selection found.");
						editor.replaceSelection(sel.replace(regex, replacement));
					} else {
						editor.setValue(editor.getValue().replace(regex, replacement));
					}

					this.settings = { ...this.settings, pattern, replacement, flags, onlySelection, replaceAll };
					await this.saveData(this.settings);
					new Notice("Regex Replace successful!");
				} catch (err) {
					new Notice("Regex error: " + err);
				}
			},
			this
		).open();
	}
};

// --- Modal for entering preset name ---
class PresetNameModal extends Modal {
	constructor(app, onSubmit) {
		super(app);
		this.onSubmit = onSubmit;
	}

	onOpen() {
		const { contentEl } = this;
		contentEl.createEl("h2", { text: "Save Preset" });

		let name = "";

		new Setting(contentEl)
			.setName("Preset name")
			.addText((t) => t.setPlaceholder("e.g. Swap Names").onChange((val) => (name = val)));

		new Setting(contentEl)
			.addButton((b) => b.setButtonText("Save").setCta().onClick(() => {
				if (name) {
					this.close();
					this.onSubmit(name);
				}
			}))
			.addButton((b) => b.setButtonText("Cancel").onClick(() => this.close()));
	}

	onClose() {
		this.contentEl.empty();
	}
}

// --- Main Modal ---
class RegexReplaceModal extends Modal {
	constructor(app, editor, settings, onSubmit, plugin) {
		super(app);
		Object.assign(this, { editor, settings, onSubmit, plugin });

		// state from settings
		this.pattern = settings.pattern;
		this.replacement = settings.replacement;
		this.flags = settings.flags;
		this.onlySelection = settings.onlySelection;
		this.replaceAll = settings.replaceAll;

		this.controls = {};
	}

	// central method: load preset into variables + UI
	loadPreset(p) {
		this.pattern = p.pattern;
		this.replacement = p.replacement;
		this.flags = p.flags;
		this.onlySelection = p.onlySelection;
		this.replaceAll = p.replaceAll;

		if (this.controls.pattern) this.controls.pattern.setValue(this.pattern);
		if (this.controls.replacement) this.controls.replacement.setValue(this.replacement);
		if (this.controls.flags) this.controls.flags.setValue(this.flags);
		if (this.controls.onlySelection) this.controls.onlySelection.setValue(this.onlySelection);
		if (this.controls.replaceAll) this.controls.replaceAll.setValue(this.replaceAll);
	}

	showPreview(previewEl) {
		previewEl.empty();
		let flags = this.flags;
		if (!flags.includes("g")) flags += "g";

		let regex;
		try {
			regex = new RegExp(this.pattern, flags);
		} catch (e) {
			return previewEl.createEl("div", { text: "Invalid regex: " + e.message });
		}

		const text = this.onlySelection ? this.editor.getSelection() : this.editor.getValue();

		const wrapper = previewEl.createEl("div", {
			attr: { style: "max-height:200px; overflow-y:auto; width:100%;" }
		});

		const table = wrapper.createEl("table", {
			attr: { style: "width:100%; min-width:100%; border-collapse:collapse; font-family:var(--font-monospace); font-size:0.9em;" }
		});

		const thead = table.createEl("thead", { attr: { style: "position:sticky; top:0; background:var(--background-secondary);" }});
		const headerRow = thead.createEl("tr");
		["Before", "After"].forEach(h =>
			headerRow.createEl("th", {
				text: h,
				attr: { style: "border:1px solid var(--background-modifier-border); padding:4px 6px; text-align:left;" }
			})
		);

		const tbody = table.createEl("tbody");
		let count = 0;

		text.replace(regex, (...args) => {
			if (count >= 50) return;

			const match = args[0];

			const startLine = text.lastIndexOf("\n", args[args.length - 2]);
			const endLine = text.indexOf("\n", args[args.length - 2] + match.length);
			const line = text.slice(startLine + 1, endLine === -1 ? undefined : endLine);

			const replacedLine = line.replace(regex, this.replacement);

			const row = tbody.createEl("tr");
			row.createEl("td", {
				text: line,
				attr: { style: "border:1px solid var(--background-modifier-border); padding:4px 6px; white-space:pre-wrap; width:50%;" }
			});
			row.createEl("td", {
				text: replacedLine,
				attr: { style: "border:1px solid var(--background-modifier-border); padding:4px 6px; white-space:pre-wrap; width:50%;" }
			});

			count++;
			return match;
		});

		previewEl.createEl("div", {
			text: count ? `${count} matches (max. 50 shown)` : "No matches found.",
			attr: { style: "margin-top:4px; font-size:0.85em; color:var(--text-muted);" }
		});
	}

	onOpen() {
		const { contentEl } = this;
		contentEl.createEl("h2", { text: "Regex Search & Replace" });

		const previewEl = contentEl.createDiv({ cls: "regex-preview" });

		let dropdown;

		// Preset Dropdown
		const presetSetting = new Setting(contentEl).setName("Saved presets");
		presetSetting.addDropdown(dd => {
			dropdown = dd;
			dd.addOption("", "-- Select preset --");
			this.settings.presets.forEach(p => dd.addOption(p.name, p.name));
			dd.onChange((val) => {
				const p = this.settings.presets.find(pr => pr.name === val);
				if (p) {
					this.loadPreset(p);
					new Notice("Preset loaded: " + val);
				}
			});
		});

		// Buttons for presets
		new Setting(contentEl)
			.addButton(b => b.setButtonText("Save as preset").onClick(() => {
				new PresetNameModal(this.app, async (name) => {
					const exists = this.settings.presets.some(p => p.name === name);
					this.settings.presets = this.settings.presets.filter(p => p.name !== name);

					const newPreset = {
						name,
						pattern: this.pattern,
						replacement: this.replacement,
						flags: this.flags,
						onlySelection: this.onlySelection,
						replaceAll: this.replaceAll
					};
					this.settings.presets.push(newPreset);
					await this.plugin.saveData(this.settings);

					if (dropdown) {
						// clear & rebuild to avoid duplicate options
						dropdown.selectEl.innerHTML = "";
						dropdown.addOption("", "-- Select preset --");
						this.settings.presets.forEach(p => dropdown.addOption(p.name, p.name));
						dropdown.setValue(name);
					}

					new Notice(exists ? `Preset overwritten: ${name}` : `Preset saved: ${name}`);
				}).open();
			}))
			.addButton(b => b.setButtonText("Delete preset").onClick(async () => {
				const current = dropdown.getValue();
				if (!current) {
					new Notice("No preset selected to delete.");
					return;
				}
				this.settings.presets = this.settings.presets.filter(p => p.name !== current);
				await this.plugin.saveData(this.settings);

				// refresh dropdown
				dropdown.selectEl.innerHTML = "";
				dropdown.addOption("", "-- Select preset --");
				this.settings.presets.forEach(p => dropdown.addOption(p.name, p.name));
				dropdown.setValue("");

				new Notice(`Preset deleted: ${current}`);
			}));

		// Input fields and toggles
		const fields = [
			{ key: "pattern", label: "Regex pattern", type: "text", val: this.pattern, set: (v) => (this.pattern = v) },
			{ key: "replacement", label: "Replacement text", type: "text", val: this.replacement, set: (v) => (this.replacement = v) },
			{ key: "flags", label: "Flags (e.g. g, i, m)", type: "text", val: this.flags, set: (v) => (this.flags = v) },
			{ key: "onlySelection", label: "Use only selection", type: "toggle", val: this.onlySelection, set: (v) => (this.onlySelection = v) },
			{ key: "replaceAll", label: "Replace all", type: "toggle", val: this.replaceAll, set: (v) => (this.replaceAll = v) },
			{ key: "preview", label: "Preview", type: "button", cta: false, handler: () => this.showPreview(previewEl) },
			{ key: "execute", label: "Execute", type: "button", cta: true, handler: () => {
				this.close();
				this.onSubmit(this.pattern, this.replacement, this.flags, this.onlySelection, this.replaceAll);
			}}
		];

		fields.forEach(f => {
			const s = new Setting(contentEl).setName(f.type !== "button" ? f.label : "");
			if (f.type === "text") {
				s.addText((t) => {
					t.setValue(f.val).onChange(f.set);
					this.controls[f.key] = t;
				});
			}
			if (f.type === "toggle") {
				s.addToggle((tg) => {
					tg.setValue(f.val).onChange(f.set);
					this.controls[f.key] = tg;
				});
			}
			if (f.type === "button") {
				s.addButton((b) => {
					b.setButtonText(f.label).onClick(f.handler);
					if (f.cta) b.setCta();
				});
			}
		});
	}

	onClose() {
		this.contentEl.empty();
	}
}


Author

Developed by Frank Wisniewski