Persistent folding

So I recently migrated to SB from Roam, and I missed the persistent folding of outline blocks. With thanks to inspiration from this topic:

I have written some Lua code to do this.

I was hoping to be able to write an event listener that would activate whenever it detected blocks being folded or unfolded, but no such event seems to be emitted. Therefore I fell back on writing my own “togglePersistentFold” command, and forgetting the existing fold/unfold commands entirely. When you use this command with the cursor in a suitable line, it adds or removes a specific character from the end of the line (a down arrow emoji) and then toggles the fold status of the line.

On page load (when the blocks you previously folded would normally reappear in their unfolded state), an event listener folds up any lines that have that character at the end so that they are in the same state as you left them.

Regex wizards could probably have done this in about two lines, but I’m not one of them! Also my first time writing Lua so this is probably horrible code, but it works.

```space-lua

persFold = persFold or {} -- define namespace for persistent fold supporting functions

persFold.marker = "⬇️"

function persFold.lineStart(pageText, pos)
    -- Return the starting position of the line of the current page containing position pos
  
        -- Get the content of the page text up to pos  
    local pageTextToPos = string.sub(pageText, 1, pos)
        -- Reverse it
    local pageTextToPosReversed = string.reverse(pageTextToPos)
        -- Find the number of characters before the first new line character
    local posPositionInLine, _ = string.find(pageTextToPosReversed, "\n") - 2
        -- Subtract that number from pos
    local startOfLine = pos - posPositionInLine
  
    return startOfLine
  
end

function persFold.lineLength(pageText, startOfLine)

    local pageTextFromStartOfLine = string.sub(pageText, startOfLine)
    local lineLength, _ = string.find(pageTextFromStartOfLine, "\n") - 1

    return lineLength
  
end

function persFold.lineText(pageText, pos)
    -- Return the text of the line of the current page containing position pos

    --local startOfLine = persFold.currentLineStart(pageText, pos)    
    --local pageTextFromStartOfLine = string.sub(pageText, startOfLine)
        -- Find the end of the current line, or the end of the page if it's the last line
    local startOfLine = persFold.lineStart(pageText, pos)
    local lineLength = persFold.lineLength(pageText, startOfLine)

  
    if lineLength == -1 then
        lineLength = string.len(pageText) - startOfLine + 1
    end

    local lineText = string.sub(pageText, startOfLine, startOfLine + lineLength - 1)
  
    return lineText
  
end
```

```space-lua
command.define {
  name = "togglePersistentFold",
  key = "ctrl-alt-q",
  run = function()

    local pageText = editor.getText()
    local cursorPositionInPage = editor.getCursor()

    -- Get the text, starting position and length of the current line
    local currentLineText = persFold.lineText(pageText, cursorPositionInPage)
    local currentLineStart = persFold.lineStart(pageText,cursorPositionInPage)
    local currentLineLength = persFold.lineLength(pageText,currentLineStart)
    
    -- Search in current line for the first instance of `* `
    local bulletPositionInLine, _ = string.find(currentLineText, "\\* ")
    
        if (bulletPositionInLine != nil) and (currentLineLength != -1) then
          -- If there is a bullet point in this line and this is not the last line
          -- Find start, length and text of next line
          local nextLineStart = currentLineStart + currentLineLength
          local nextLineLength = persFold.lineLength(pageText,nextLineStart)
          local nextLineText = persFold.lineText(pageText,nextLineStart)

          -- Find bullet position in next line
          local bulletPositionInNextLine, _ = string.find(nextLineText, "\\* ")
          -- Is it indented further than the current line's bullet, if there is a bullet at all? (If there isn't, string.find returns 0, so the same expression tests for both cases.)
          if bulletPositionInNextLine > bulletPositionInLine then
            -- Now we are sure we are on a line which has a bullet point and has a sub-bullet in the next line.
            -- Check if this line has the fold marker at the end
            
            if string.sub(currentLineText,currentLineLength - string.len(persFold.marker) + 1, currentLineLength) == persFold.marker then
              -- If yes, remove marker
              editor.replaceRange(nextLineStart - 1 - string.len(persFold.marker), nextLineStart - 1, "")
            else
              -- If no, add marker
              editor.insertAtPos(persFold.marker, nextLineStart - 1)
            end

            editor.toggleFold()
            
          end
        end
  end
}

-- Event listener to fold up marked bullets on page load
event.listen {
  name = "editor:pageLoaded",
  run = function()

    -- move backward through the page looking for the fold marker at the end of a line, and fold whenever you find it
    -- (it might work going forward, but it was easier to write it this way than to test.)
    -- repeat until you can't find the fold marker at the end of any more lines

    local markerNewline = persFold.marker .. "\n"
    local pageText = editor.getText()
    pageTextReversed = string.reverse(pageText)
    nextFold, _ = string.find(pageTextReversed, string.reverse(markerNewline))

    while nextFold > 0 do

      editor.moveCursor(string.len(pageText)-nextFold, false)
      editor.toggleFold()
      nextFold, _ = string.find(pageTextReversed, string.reverse(markerNewline), nextFold + 1)
      
    end

    editor.moveCursor(0) -- put the cursor back at the top of the page
    
  end
}
```

As written, it requires nested bullet points to be on strictly successive lines, which is not quite in accordance with SB’s folding logic. Close enough for me, for now…

EDIT: changed a 1 to a 0 in the last line there, 1 caused an out of range error on empty pages.
Edit by @zef: Helped you a bit making it clear you’re using space-lua blocks here

3 Likes

I’d have to look at the details, but very happy to see people using Lua scripting to do this stuff now. This is exciting! Nice :+1:

Thanks Zef!