Folding Markdown headings
Description of the bug
A general question about syntax based folding is how to achieve something like folding markdown headings of certain levels via fold buttons.
MarkdownEditing can already fold sections via key bindings. It would be cool to bind those functions to the fold buttons of a heading or achieve same behavior using syntax based folding.
A PoC of a Fold.tmPreferences is attached below, but it seems not to work as expected.
Steps to reproduce
- Open ST
- Create Packages/Markdown/Fold.tmPreferences using snippet below
- Paste test content to new buffer and assign Markdown syntax
- try folding sections using fold buttons
Expected behavior
Folding a Heading of leval x should fold everything until a heading of level x or x-n is found.
Seems to work correctly for headings of level 3.

Actual behavior
Behavior fails for heandings of level x if followed by headings of level x+n | n>0.
See how heading 2 and heading 3 fold as expected, but heading 1 or heading 1.1 don't.

Sublime Text build number
4133
Operating system & version
Windows 11
(Linux) Desktop environment and/or window manager
No response
Additional information
Markdown/Fold.tmPreferences
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>scope</key>
<string>text.html.markdown</string>
<key>settings</key>
<dict>
<key>foldScopes</key>
<array>
<dict>
<key>begin</key>
<string>markup.heading.1 meta.whitespace.newline</string>
<key>end</key>
<string>markup.heading.1 & punctuation.definition.heading</string>
<key>excludeTrailingNewlines</key>
<true/>
</dict>
<dict>
<key>begin</key>
<string>markup.heading.2 meta.whitespace.newline</string>
<key>end</key>
<string>(markup.heading.2 | markup.heading.1) & punctuation.definition.heading</string>
<key>excludeTrailingNewlines</key>
<true/>
</dict>
<dict>
<key>begin</key>
<string>markup.heading.3 meta.whitespace.newline</string>
<key>end</key>
<string>(markup.heading.3 | markup.heading.2 | markup.heading.1) & punctuation.definition.heading</string>
<key>excludeTrailingNewlines</key>
<true/>
</dict>
<dict>
<key>begin</key>
<string>markup.heading.4 meta.whitespace.newline</string>
<key>end</key>
<string>(markup.heading.4 | markup.heading.3 | markup.heading.2 | markup.heading.1) & punctuation.definition.heading</string>
<key>excludeTrailingNewlines</key>
<true/>
</dict>
<dict>
<key>begin</key>
<string>markup.heading.5 meta.whitespace.newline</string>
<key>end</key>
<string>(markup.heading.5 | markup.heading.4 | markup.heading.3 | markup.heading.2 | markup.heading.1) & punctuation.definition.heading</string>
<key>excludeTrailingNewlines</key>
<true/>
</dict>
<dict>
<key>begin</key>
<string>markup.heading.6 meta.whitespace.newline</string>
<key>end</key>
<string>(markup.heading.6 | markup.heading.5 | markup.heading.4 | markup.heading.3 | markup.heading.2 | markup.heading.1) & punctuation.definition.heading</string>
<key>excludeTrailingNewlines</key>
<true/>
</dict>
</array>
</dict>
</dict>
</plist>
Test File
# 1 Heading
para
## 1.1 Heading
para
### 1.1.1 Heading
para
## 1.2 Heading
para
### 1.2.1 Heading
para
# 2 Heading
para
# 3 Heading
para
# 4 Heading
para
OpenGL context information
No response
I'm glad I found this, I just finished leaving a comment on #101 with this same puzzling behavior.
The explanation seems to be that the region-end detection has no memory, so when it uses (in my syntax) markup.section.3.begin and runs across a markup.section.4.begin, it gets confused and folds everything to the end of the file
This looks like a design flaw with the foldScopes approach, because while it could maintain a stack for a well-behaved system like you and I are trying to describe (nested subsections), the begin and end keys aren't guaranteed to nest, if we have an a scope and a b scope, these can appear as abab, not just the desirable abba.
A proposal would be to have a foldContexts property, which works with contexts. Contexts already work on a stack and it's tractable to set up contexts which mirror a tree structure exactly.
Here's another attempt to fold markdown headings per level.
Folding a level 2 heading works, if level 3 headings are already folded.
Otherwise excludeTrailingNewlines doesn't apply and the next heading is placed at the same line as the folded heading.
Fold.tmPreferences
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>scope</key>
<string>text.html.markdown</string>
<key>settings</key>
<dict>
<key>foldScopes</key>
<array>
<dict>
<key>begin</key>
<string>markup.heading.1 meta.whitespace.newline - markup.quote - meta.list</string>
<key>end</key>
<string>
markup.heading.1 - markup.quote - meta.list
</string>
<key>excludeTrailingNewlines</key>
<true/>
</dict>
<dict>
<key>begin</key>
<string>markup.heading.2 meta.whitespace.newline - markup.quote - meta.list</string>
<key>end</key>
<string>
markup.heading.2 - markup.quote - meta.list,
markup.heading.1 - markup.quote - meta.list
</string>
<key>excludeTrailingNewlines</key>
<true/>
</dict>
<dict>
<key>begin</key>
<string>markup.heading.3 meta.whitespace.newline - markup.quote - meta.list</string>
<key>end</key>
<string>
markup.heading.3 - markup.quote - meta.list,
markup.heading.2 - markup.quote - meta.list,
markup.heading.1 - markup.quote - meta.list
</string>
<key>excludeTrailingNewlines</key>
<true/>
</dict>
<dict>
<key>begin</key>
<string>markup.heading.4 meta.whitespace.newline - markup.quote - meta.list</string>
<key>end</key>
<string>
markup.heading.4 - markup.quote - meta.list,
markup.heading.3 - markup.quote - meta.list,
markup.heading.2 - markup.quote - meta.list,
markup.heading.1 - markup.quote - meta.list
</string>
<key>excludeTrailingNewlines</key>
<true/>
</dict>
<dict>
<key>begin</key>
<string>markup.heading.5 meta.whitespace.newline - markup.quote - meta.list</string>
<key>end</key>
<string>
markup.heading.5 - markup.quote - meta.list,
markup.heading.4 - markup.quote - meta.list,
markup.heading.3 - markup.quote - meta.list,
markup.heading.2 - markup.quote - meta.list,
markup.heading.1 - markup.quote - meta.list
</string>
<key>excludeTrailingNewlines</key>
<true/>
</dict>
<dict>
<key>begin</key>
<string>markup.heading.6 meta.whitespace.newline - markup.quote - meta.list</string>
<key>end</key>
<string>
markup.heading.6 - markup.quote - meta.list,
markup.heading.5 - markup.quote - meta.list,
markup.heading.4 - markup.quote - meta.list,
markup.heading.3 - markup.quote - meta.list,
markup.heading.2 - markup.quote - meta.list,
markup.heading.1 - markup.quote - meta.list
</string>
<key>excludeTrailingNewlines</key>
<true/>
</dict>
<dict>
<key>begin</key>
<string>meta.code-fence.definition.begin</string>
<key>end</key>
<string>meta.code-fence.definition.end</string>
<key>excludeTrailingNewlines</key>
<true/>
</dict>
</array>
</dict>
</dict>
</plist>