sublime_text icon indicating copy to clipboard operation
sublime_text copied to clipboard

Folding Markdown headings

Open deathaxe opened this issue 3 years ago • 3 comments

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

  1. Open ST
  2. Create Packages/Markdown/Fold.tmPreferences using snippet below
  3. Paste test content to new buffer and assign Markdown syntax
  4. 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.

Animation

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.

Animation

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

deathaxe avatar May 27 '22 10:05 deathaxe

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.

mnemnion avatar Aug 05 '22 19:08 mnemnion

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.

Animation

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>

deathaxe avatar Jun 03 '23 15:06 deathaxe