[space-lua] Toggle / Rotate Header Level h1-h6-On/Off

I was having a “productive conversation” with ChatGPT today and after a couple of back and forth, together we cooked up these two little handy Shortcut-Commands:

First one is to toggle h1-h6 level headers with one convenient combo-keypress (Ctrl-1 to Ctrl-6):

-- function to toggle a specific header level
local function toggleHeader(level)
  local line = editor.getCurrentLine()

  -- handle empty line
  if not line or not line.text then
    editor.insertAtCursor(string.rep("#", level) .. " ")
    return
  end

  local s, from, to = line.text, line.from, line.to

  local prefix, rest = s:match("^(#+)%s+(.*)")
  local currentLevel = prefix and #prefix or 0

  local newLine
  if currentLevel == level then
    newLine = rest or s  -- remove header
  else
    newLine = string.rep("#", level) .. " " .. (rest or s)  -- set header to desired level
  end

  editor.replaceRange(from, to, newLine)
  editor.moveCursor(from + #newLine, false)
end

-- register commands Ctrl-1 → Ctrl-6
for lvl = 1, 6 do
  command.define {
    name = "Header: Toggle Level - h" .. lvl,
    key = "Ctrl-" .. lvl,
    run = function() toggleHeader(lvl) end
  }
end

The second one to rotate header level between [ h1-h2-h3-h4-h6-off ] using Ctrl-Shift-Tab:

command.define {
  name = "Header: Rotate level",
  key = "Ctrl-Shift-Tab",
  run = function()
    local line = editor.getCurrentLine()
    
    -- If the line is completely empty (or nil), insert "# " at cursor
    if not line or not line.text then
      local cursor = editor.getCursor() or 0
      editor.insertAtCursor("# ")
      return
    end

    -- Normal case
    local s, from, to = line.text, line.from, line.to

    local hashes, rest = s:match("^(#+)%s+(.*)")
    local level = hashes and #hashes or 0

    local newLine = level < 6 and string.rep("#", level + 1) .. " " .. (rest or s) or (rest or s)

    editor.replaceRange(from, to, newLine)
    editor.moveCursor(from + #newLine, false)
  end
}

Feel free to modify the Shortcut keys to your liking, also i am not a coder or a developer so you can also comment and/or improve the script itself, and also leave some feedback if you think it´s useful for you or not :wink:

Interesting. Are you aware that the /h1 - /h4 slash commands update your current line to the desired level? This is what I always use.

2 Likes

yes but that’s a couple of keypresses more:

“Space” + “/” + “h” + “1” + “Enter” - 5x keypresses (both hands used)

vs.

“Ctrl” + “1” - 1x ComboKey press (one hand)

And also the /slash command doesn’t turn the current header level off unless you manually delete the “######” in front of the header(and that could be a lot of keypresses :stuck_out_tongue: )
I was going for a quicker shortcut and not necessarily a “missing feature”.

BTW, is there a way to attach a shortcut key to a /slash command like in v1 ? That is what I was trying to mimic with my two commands. If there was a way to assign Shortcut Keys to /slash commands too, that would be awesome. (I didn’t find anything in the docs)

2 Likes

Here is an updated version of the Header: Toggle Level commands.
This is derived from the “built-in” /h1-/h5 slash commands and added the part to handle the toggling off. More straightforward and still retains all the functions.

-- function to toggle a specific header level

local function toggleHeader(level)
  local line = editor.getCurrentLine()
  local text = line.textWithCursor

  -- Detect current header level
  local currentLevel = string.match(text, "^(#+)%s*")
  currentLevel = currentLevel and #currentLevel or 0

  local cleanText = string.gsub(text, "^#+%s*", "")

  -- Toggle: remove if same, otherwise set new level
  if currentLevel == level then
    editor.replaceRange(line.from, line.to, cleanText, true)
  else
    editor.replaceRange(line.from, line.to, string.rep("#", level) .. " " .. cleanText, true)
  end
end

-- register commands Ctrl-1 → Ctrl-6
for lvl = 1, 6 do
  command.define {
    name = "Header: Toggle Level " .. lvl,
    key = "Ctrl-" .. lvl,
    run = function() toggleHeader(lvl) end
  }
end
2 Likes

Very productive. Thanks.

did some clean up for @Mr.Red

now behaves correctly with cursor inside the ###... structure.

-- function to toggle a specific header level
local function toggleHead(level)
  local line = editor.getCurrentLine()
  
  local text = line.text
  local currentLevel = string.match(text, "^(#+)%s*")
  currentLevel = currentLevel and #currentLevel or 0
  
  local textC = line.textWithCursor
  local posC = string.find(textC, "|^|", 1, true)
  
  local lineHead
  if posC > currentLevel + 1 then
    local bodyTextC = string.gsub(textC, "^#+%s*", "")
    if currentLevel == level then
      lineHead = bodyTextC
    else
      lineHead = string.rep("#", level) .. " " .. bodyTextC
    end
  else
    local bodyText = string.gsub(text, "^#+%s*", "")
    if currentLevel == level then
      lineHead = "|^|" .. bodyText
    else
      lineHead = string.rep("#", level) .. " |^|" .. bodyText
    end
  end
  editor.replaceRange(line.from, line.to, lineHead, true)
end

-- register commands Ctrl-1 → Ctrl-6
for lvl = 1, 6 do
  command.define {
    name = "Header: Toggle Level " .. lvl,
    key = "Ctrl-" .. lvl,
    run = function() 
      toggleHead(lvl) 
    end
  }
end
3 Likes

thanks, just updated my library with your fixes. :+1:

1 Like