Sometimes I copy material from one note into another, or within a note. Because of its new place in the note, the headers need to be changed. E.g., in one place a sequence of headers might be:
A
A1
A2
B
B1
But once I copy this into a new section of note beginning
Lists starting with capital letters
I now need to “demote” everything:
Lists starting with capital letters
A
A1
A2
B
B1
Proposed solution
I would be great to be able to do this with a single hotkey rather than changign each heading manually.
Hi @JAndrews2, this is a very fair feature request IMHO. Just like we can tab and shift+tab outlines, we should be able to do the same with headings.
While we are waiting for an official feature like this, I wrote a little script to do it in the meantime. I’m just beginning to learn some javascript, so this was a fun exercise.
Script to promote/demote all headings in selection
<%*
const direction = 1;
// 1: Promote headings (make them bigger)
// -1: Demote headings (make smaller)
const editor = app.workspace.activeLeaf.view.editor;
let selection = editor.getSelection();
if (selection=="") {
// if there is no selection, set it to the current line instead
let l = editor.getCursor().line;
editor.setSelection({line: l, ch: 0}, {line: l, ch: 99});
selection = editor.getSelection();
}
/* Because this script is being called from Templater, the selection is automatically replaced with the text of this script before being executed and consumed. The next three lines reset the selection to what it was before execution. For some reason a timer of 0ms is sufficient for that. */
let cAnchor = editor.getCursor('anchor');
let cHead = editor.getCursor('head');
setTimeout(() => editor.setSelection(cAnchor, cHead), 0);
if (direction == 1)
editor.replaceSelection(selection.replace(/^#(#+) /gm, "$1 "));
else if (direction == -1)
editor.replaceSelection(selection.replace(/^#+ /gm, "#$&"));
%>
Create two new files in your template folder (specified in Templater settings)
Copy and paste the above script into each of the two files, and changeconst direction to -1 in only one of them (that will be your “demote”)
Enable those templates in “Hotkeys for templates” settings
Assign hotkeys to the command via Obsidian settings > Hotkeys
You could also set these up as macro choices in QuickAdd. A little more complicated with QuickAdd, but then you could avoid the strange setTimeout(... 0) that I don't fully understand.
And if someone can improve my code, all the better!
Demo
Sorry my custom css may make it a little harder to see what’s going on.
Hi @scwunch, I have been following the instructions you have given above, so I installed the two plugins, pasted the script into the two files in the template folder, and they looked jammed without all the line breaks, but I went ahead and run the templates anyway, but it only replaced whatever selection of text with the actual content of that template(which is the script itself). Is there any setting I need to toggle for the template to run as commands?
Basically, the plugin allows us to apply customized regex to highlighted text. This is convenient for some tasks.
Here are the steps to promote/demote all selected headers.
Install the plugin
Copy below code to clipboard
Click “Import patterns from clipboard” in Apply Patterns's option page.
Then enjoy the solution
highlight any text range
trigger “Apply Patterns: Apply patterns to selection”, you will see two commands “refactor: increase heading level” and “refactor: decrease heading level”
Brilliant! Just what the doctor ordered. Hopefully it gets baked in the core, one fine day. (He said, with an already-overflowing plate and a mouthful of food.)
Another solution is to place multiple cursors (alt+click) on each header, go to the beginning of the line using the keyboard key Home and hit # to demote, or hit Delete to promote!
You can also create the multiple cursors with more ease using Advanced Cursors (Obsidian Store, GitHub).
and another another solution: use the plugin obsidian-linter:
in my case I head many H5s under one H4 which I had to change to a H3 (and with it all the belonging H5s).
I only had to change the H4 to H3 and the linter automatically changed all lower H5s to H4s.
I met the same problem with @Heisenberg, when trigger the hotkey, my seleted text is changed by the content of the script, rather than fommating. I named two files like a.md and b.md, and copy your script into them, and then set the hotkeys.
This works fine to me, I am currently using it. The only problem that I met is my tag
which is something like #abc is also changed. I am trying to update the code to adapt this.
Hi
Thank you for this solution.
For some reason in my case, the code simply replace the heading (including all subheadings and content) by itself not the increase of decrease the # character.
I feel like the first part of the code work but not the second part.
PS: I am new to obsidian and running scripts inside .md.
can be used to set headings to specific level, or increase/decrease
multi-line: increase/decrease all headings in multi-line selection
“promote” defaults to heading level 3 if no heading in selection
preserve cursor position / selection
Script
/* Set the heading to the specified level.
* If the level is negative, will decrease heading size by that much.
* If the level is 0 or undefined, will increase the heading by one, or turn a non-heading into a level-3 heading
*/
function setHeading(level) {
const editor = app.workspace.activeLeaf.view.editor;
const cAnchor = editor.getCursor('anchor');
const cHead = editor.getCursor('head');
const top = {line: Math.min(cAnchor.line, cHead.line), ch: 0};
const bottom = {line: Math.max(cAnchor.line, cHead.line), ch: Infinity};
const lineRange = editor.getRange(top, bottom);
let replacement = lineRange;
if (!level) {
// promote all headings in lineRange. If no heading in lineRange, promote all lines to h3
if (!lineRange.match(/^#+ /m)) {
replacement = lineRange.replace(/^/gm, "### ");
}
else { // otherwise promote by 1
replacement = lineRange.replace(/^#(#+) /gm, "$1 ");
}
}
else if (level < -1)
console.error("Heading level of less than -1 is not supported, demoting heading(s) by one only. Aborting.");
else if (level == -1) {
replacement = lineRange.replace(/^#+ /gm, "#$&");
}
else {
// set lineRange to header level
console.log(`set header to level: ${level}.`);
if (!lineRange.match(/^#+ /m)) {
console.log('no headings found, replacing all lines')
replacement = lineRange.replace(/^/gm, "#".repeat(level)+" ");
}
else {
console.log('replace certain headings')
replacement = lineRange.replace(/^#+ (.*)/gm, "#".repeat(level) + " $1");
}
}
editor.replaceRange(replacement, top, bottom);
// adjust new selection
let lines = lineRange.split('\n');
let newLines = replacement.split('\n');
[cAnchor, cHead].forEach(p => {
let i = p.line - top.line;
if (p.ch)
p.ch += newLines[i].length - lines[i].length;
});
editor.setSelection(cAnchor, cHead);
return;
}
module.exports = setHeading;
Instructions
Install Templater
Create a file called “setHeading.js” in your vault (I suggest putting it in a folder called “Scripts”)
copy and paste the content (Script) from above
you will have to use a text editor other than Obsidian to create the js file
Create two markdown files in your templates folder with the following content:
<%*
tp.user.setHeading();
return;
%>
and
<%*
tp.user.setHeading(-1);
return;
%>
you can also use numbers 1 – 6 to set headings to a specific level
Name the files something like “Increase Headings” and “Decrease Headings”
In Templater Settings, make sure you can see tp.user.setHeading in the User Script Functions section.
Add a Template Hotkey for each of the templates you just created and set to your desired shortcuts in Obsidian Settings > Hotkeys