Now, first of all, this functionality is currently missing and there are also workarounds as suggested in this thread, although some plugins do the replace but not the search.
In the meantime, I found another attempt to find a remedy.
I myself posted at least a couple of solutions using Templater for either search or replace, but it was either a solution for querying the full vault or some niche case for replacing in the current file.
Secondly, people who don’t know what regex is may not find it useful, although the script works for normal search and replace, which the program can do internally anyway (CNTRL+H).
- Well, it can, but currently you cannot switch case on it. Bummer. There regex search and replace is purposefully case sensitive.*
I always hesitate before wanting to post scripts here because people who know regex can probably cook a simple script like this up on their own, while people who don’t know (of) regex, will not even make use of this, but anyway… I had some time today…
People with no regex knowledge or no wish to learn it can still make use of the third script: the benefit of querying like this is that you can take in all results at once.
I am going to put this up for the benefit of anyone who wanted especially a regex search option within a current file. Workarounds exist even for that, but I think many people are shying away from typing in the search modal the filename using the file:
search operator, including myself.
Still, for the search part, we’re going to use inline queries and the same syntax, but the script takes the filename of the open file automatically.
Before I share the script…what it does is simple enough: you can search or replace strings, and on both ends regex patterns are accepted (the main idea behind writing this, in any case).
When searching only, the script expects a valid YAML frontmatter, because the insertion of the inline query happens below the YAML (this is convenient for more than one reason). I expect that since the introduction of Properties last August (or so), most everyone has a YAML frontmatter.
When the search and replace happens, the script updates the date modified
field (it checks for YAML here, because I don’t want to insert this date modified property in Templater files, for instance). If this is unwanted, the user can delete these parts. I will offer different scenarios, so people can decide on what they want to use.
The full script:
<%*
//Switch to Live preview
const view = app.workspace.activeLeaf.getViewState()
view.state.mode = 'source'
view.state.source = false
app.workspace.activeLeaf.setViewState(view)
// Get the current file
const currentFile = app.workspace.getActiveFile();
if (!currentFile) return;
const fileContent = await app.vault.read(currentFile);
const separator = '---';
const firstSeparatorPos = fileContent.indexOf(separator);
const secondSeparatorPos = fileContent.indexOf(separator, firstSeparatorPos + 1);
// Ask user whether to search or replace
const userChoice = await tp.system.suggester(
["Search", "Replace"],
["Search", "Replace"],
false,
"Choose an option:"
);
if (!userChoice) return;
// Ask user for a search pattern – regex pattern is accepted
const userPattern = await tp.system.prompt("Enter search pattern:");
if (!userPattern) return;
if (userChoice.toLowerCase() === "replace") {
// Ask user for a replacement pattern
const userReplace = await tp.system.prompt("Enter replacement pattern (or leave empty for zero replacement):");
// if (!userReplace) return;
if (userReplace === undefined) return;
// Now pressing enter in empty box to select nothing as replacement works
// Perform search and replace
const updatedContent = fileContent.replace(new RegExp(userPattern, 'gm'), userReplace);
// Write back changes
await app.vault.modify(currentFile, updatedContent);
// Check for the existence of YAML and update date modified
const yamlRegex = /^---\s*\n[\s\S]*?\n---/;
const hasYaml = yamlRegex.test(fileContent);
if (hasYaml) {
// Define an async function to update date modified
(async () => {
await new Promise(resolve => setTimeout(resolve, 2200));
const modDateTime = await tp.date.now("YYYY-MM-DD");
await app.fileManager.processFrontMatter(tp.config.target_file, frontmatter => {
frontmatter['date modified'] = modDateTime;
});
})();
}
} else {
// Prepare dynamic content with the user's regex pattern
const dynamicContent = `
\%\%
\`\`\`query
file: /^${currentFile.basename}.md/ /${userPattern}/
\`\`\`
\%\%`;
// Ensure an extra empty line after the YAML separator for search
const updatedContent = fileContent.slice(0, secondSeparatorPos + separator.length) +
`\n${dynamicContent}` +
fileContent.slice(secondSeparatorPos + separator.length);
// Insert inline query
await app.vault.modify(currentFile, updatedContent);
// Set the cursor to the beginning of the query
this.app.workspace.activeLeaf.view.editor.setCursor({ line: 1, ch: 0 });
}
_%>
If your date modified
YAML key is modifed
, you simply remove date
from the script, or if you have other date format, modify the relevant part (I myself have switched to YYYY-MM-DDTHH:mm
now). Otherwise if you don’t want to update YAML, you can remove the whole bit and use:
<%*
//Switch to Live preview
const view = app.workspace.activeLeaf.getViewState()
view.state.mode = 'source'
view.state.source = false
app.workspace.activeLeaf.setViewState(view)
// Get the current file
const currentFile = app.workspace.getActiveFile();
if (!currentFile) return;
const fileContent = await app.vault.read(currentFile);
const separator = '---';
const firstSeparatorPos = fileContent.indexOf(separator);
const secondSeparatorPos = fileContent.indexOf(separator, firstSeparatorPos + 1);
// Ask user whether to search or replace
const userChoice = await tp.system.suggester(
["Search", "Replace"],
["Search", "Replace"],
false,
"Choose an option:"
);
if (!userChoice) return;
// Ask user for a search pattern – regex pattern is accepted
const userPattern = await tp.system.prompt("Enter search pattern:");
if (!userPattern) return;
if (userChoice.toLowerCase() === "replace") {
// Ask user for a replacement pattern
const userReplace = await tp.system.prompt("Enter replacement pattern (or leave empty for zero replacement):");
// if (!userReplace) return;
if (userReplace === undefined) return;
// Now pressing enter in empty box to select nothing as replacement works
// Perform search and replace
const updatedContent = fileContent.replace(new RegExp(userPattern, 'gm'), userReplace);
// Write back changes
await app.vault.modify(currentFile, updatedContent);
} else {
// Prepare dynamic content with the user's regex pattern
const dynamicContent = `
\%\%
\`\`\`query
file: /^${currentFile.basename}.md/ /${userPattern}/
\`\`\`
\%\%`;
// Ensure an extra empty line after the YAML separator for search
const updatedContent = fileContent.slice(0, secondSeparatorPos + separator.length) +
`\n${dynamicContent}` +
fileContent.slice(secondSeparatorPos + separator.length);
// Insert inline query
await app.vault.modify(currentFile, updatedContent);
// Set the cursor to the beginning of the query
this.app.workspace.activeLeaf.view.editor.setCursor({ line: 1, ch: 0 });
}
_%>
For those who use a plugin for replacements and want to use only the regex query, use (slightly faster as there is no drop-down menu here):
<%*
//Switch to Live preview
const view = app.workspace.activeLeaf.getViewState()
view.state.mode = 'source'
view.state.source = false
app.workspace.activeLeaf.setViewState(view)
const currentFile = app.workspace.getActiveFile();
if (!currentFile) return;
// Ask user for a search pattern – regex pattern is accepted
const userPattern = await tp.system.prompt("Enter search pattern:");
if (!userPattern) return;
// Prepare dynamic content with the user's regex pattern
const dynamicContent = `
\%\%
\`\`\`query
file: /^${currentFile.basename}.md/ /${userPattern}/
\`\`\`
\%\%`;
// Here the backtick and semicolon were brought up one level to make sure there is only one empty line before Heading1, otherwise there were always two
// Insert dynamic content below YAML section
const fileContent = await app.vault.read(currentFile);
const separator = '---';
const firstSeparatorPos = fileContent.indexOf(separator);
const secondSeparatorPos = fileContent.indexOf(separator, firstSeparatorPos + 1);
if (secondSeparatorPos !== -1) {
// Ensure an extra empty line after the YAML separator
const updatedContent = fileContent.slice(0, secondSeparatorPos + separator.length) +
`\n${dynamicContent}` +
fileContent.slice(secondSeparatorPos + separator.length);
await app.vault.modify(currentFile, updatedContent);
// Set the cursor to the beginning of the query
this.app.workspace.activeLeaf.view.editor.setCursor({line: 1, ch: 0});
}
_%>
For this to work, put the script in your Templater folder and register it by assigning a hotkey. On mobile, you can set an icon for it on the Mobile Toolbar.
If anyone says that query results are slow coming in, it is because the inline query searches all files in the vault alphabetically. It is a known thing. So if you are in a file whose filename starts with a w, search is slower than if it starts with a d, for instance.
It is good practice when starting up Obsidian to search for some random string (e.g. work) in order to cache in the contents of all files, especially if it’s a large vault.
EDIT.
The inline query results are only shown in Live Preview. One needs to switch to that before firing the script. Otherwise one can also include these lines to the front of the script (below <%*
):
//Switch to Live preview
const view = app.workspace.activeLeaf.getViewState()
view.state.mode = 'source'
view.state.source = false
app.workspace.activeLeaf.setViewState(view)
- I included these lines now above for good measure.
EDIT 2.
Fixed replacement box allowing zero string to be registered (zero/nothing on replacement side is used to delete stuff). Previously, script terminated without performing the replacement.
EDIT X. and X+1:
Added another link and a date-time format.
EDIT (last time?): *Added an important point about the built-in search and replace function being case insensitive. My script changes strings sensitively.