DEV Community

Frank Wisniewski
Frank Wisniewski

Posted on

# Regex Replace Plugin for Obsidian

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 😊

Screenshot


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/
Enter fullscreen mode Exit fullscreen mode

The folder should contain:

   manifest.json
   main.js
Enter fullscreen mode Exit fullscreen mode
  1. Restart or reload Obsidian.

  2. 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
Enter fullscreen mode Exit fullscreen mode

Search pattern

^(\w+) (\w+)$\n^(\w+)
Enter fullscreen mode Exit fullscreen mode

Replacement text

$2, $1, 53518 $3
Enter fullscreen mode Exit fullscreen mode

Result

Frank, Wisniewski, 53518 Adenau
Frank, Wisniewski, 53518 Adenau
Enter fullscreen mode Exit fullscreen mode

Example 2: Replace All i With x

Text

This is a simple line
With multiple words
Enter fullscreen mode Exit fullscreen mode

Search pattern

i
Enter fullscreen mode Exit fullscreen mode

Replacement text

x
Enter fullscreen mode Exit fullscreen mode

Result

Thxs xs a sxmple lxne
Wxth multxple words
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Download

main.js

manifest.json


Author

Developed by Frank Wisniewski
A simple plugin for Obsidian.

Top comments (0)