Promote/demote all selected headers

Use case or problem

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.

Current workaround (optional)

I add/remove the #s manually for each line.

9 Likes

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, "#$&"));

%>

How to implement

  1. Install two plugins: Templater & Hotkeys for templates
  2. Create two new files in your template folder (specified in Templater settings)
  3. 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”)
  4. Enable those templates in “Hotkeys for templates” settings
  5. 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.
promote-demote-headings

3 Likes

You are a miracle worker! Thank you so much!

My pleasure :slightly_smiling_face:

This would be a fantastic feature, and I’m eager to use something link this natively in Obsidian. Thanks for the workaround @scwunch !

1 Like

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?

Hi @Heisenberg,

Did you make sure to include the <%* and %> at the beginning and end respectively? How are you calling the script?

I love the solution by @scwunch. I didn’t know the Templater plugin before, this is an amazing solution and opens up unlimited possibilities.

At the same time, I just discovered another easier solution leveraging jglev/obsidian-apply-patterns-plugin: An Obsidian plugin for applying patterns of find and replace in succession. (github.com).

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”
[
  {
    "name": "refactor: increase heading level",
    "rules": [
      {
        "from": "^#",
        "to": "##",
        "caseInsensitive": false,
        "global": true,
        "multiline": true,
        "sticky": false,
        "disabled": false
      }
    ],
    "collapsed": false,
    "cursorRegexStart": "$",
    "cursorRegexEnd": "^"
  },
  {
    "name": "refactor: decrease heading level",
    "rules": [
      {
        "from": "^#",
        "to": "",
        "caseInsensitive": false,
        "global": true,
        "multiline": true,
        "sticky": false,
        "disabled": false
      }
    ],
    "collapsed": false,
    "cursorRegexStart": "$",
    "cursorRegexEnd": "^"
  }
]
6 Likes

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.)

1 Like

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).

2 Likes

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.

That’s at least one solution to once case :slight_smile: