Overview
Regex Replace is a plugin for Obsidian 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 😊
Installation
Download the plugin files (
manifest.json
andmain.js
).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
Restart or reload Obsidian.
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 forRegex 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
- Configure your regex pattern, replacement, and options.
- Click "Save as preset".
- Enter a preset name.
- 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
- Select a preset from the dropdown.
- The input fields (
pattern
,replacement
,flags
, etc.) will automatically update. - Preview and Execute now use these loaded values.
Deleting a Preset
- Select the preset in the dropdown.
- Click "Delete preset".
- 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();
}
}
Download
Author
Developed by Frank Wisniewski
A simple plugin for Obsidian.
Top comments (0)