Add folding on indent levels for plain text and yaml language

Hi, I’m using v6 and have been struggling for a while to add a folding feature.
I want to add the possibility to fold on indent level with small buttons on the gutter (like in other editors, exp vs-code).

It works if I load the javascript language as an extension, but only for js code. My problem is I mainly use codemirror for editing yaml files or plain textfiles and using the yaml language extension from the legacy module ( GitHub - codemirror/legacy-modes: Collection of ported legacy language modes for the CodeMirror code editor ). It seems it did not come with the folding logic?

I found so many things, but the closest to my question is an older topic ( Indentation and folding without a language ).

So my question is, how can I achive an folding on indent levels?
I have already experimented with indentService , foldService , foldNodeProp , but unfortunately can’t get them to work.

My goal would be to be able to add an extension that can be loaded alongside yaml.

A foldService is what you’d need to implement to get something like indentation-based folding. It should work with the fold gutter automatically.

Thank you for pointing this out. I have already tried with the service, but I will have a closer look next week.
So the idea behind it is to register a service as an extension, to which I give a function that determines the “from/to” value, correct?

Thanks again for your help. I worked my way through it and found a solution. Who wants a similar behavior can use the following code. With this it is possible to fold on deeper indent level. Since it is still important for our use case, the folding starts with the next line (but can be removed).

foldOnIndent.ts:

import { foldService } from '@codemirror/language'

export default function foldOnIndent() {
    return foldService.of((state, from, to) => {
        const line = state.doc.lineAt(from) // First line
        const lines = state.doc.lines // Number of lines in the document
        const indent = line.text.search(/\S|$/) // Indent level of the first line
        let foldStart = from // Start of the fold
        let foldEnd = to // End of the fold

        // Check the next line if it is on a deeper indent level
        // If it is, check the next line and so on
        // If it is not, go on with the foldEnd
        let nextLine = line
        while (nextLine.number < lines) {
            nextLine = state.doc.line(nextLine.number + 1) // Next line
            const nextIndent = nextLine.text.search(/\S|$/) // Indent level of the next line

            // If the next line is on a deeper indent level, add it to the fold
            if (nextIndent > indent) {
                foldEnd = nextLine.to // Set the fold end to the end of the next line
            } else {
                break // If the next line is not on a deeper indent level, stop
            }
        }

        // If the fold is only one line, don't fold it
        if (state.doc.lineAt(foldStart).number === state.doc.lineAt(foldEnd).number) {
            return null
        }

        // Set the fold start to the end of the first line
        // With this, the fold will not include the first line
        foldStart = line.to

        // Return a fold that covers the entire indent level
        return { from: foldStart, to: foldEnd }
    })
}

codemirror.vue:

import foldOnIndent from '@/.../foldOnIndent'
...
// Create your extension array and register it on codemirror
const extensions = [foldOnIndent()]
...

Oh and as you mentioned, the icons to collapse are automatically displayed in the Gutter.

I found out that with your example site you can create your own examples.
Here is a link with a working example:
Codemirror Folding on Indent example

3 Likes

Thank you so much for your example @borsTiHD, it saved me a lot of time! Can I ask how you implemented yaml language support?

Hi, sorry for the late response (just stumbled across my old thread).
But sure, here is a short way to get yaml language support with the legacy mode in v6.

language.ts:

import { StreamLanguage } from '@codemirror/language'
import { yaml } from '@codemirror/legacy-modes/mode/yaml'

export default function useLanguage() {
    return StreamLanguage.define(yaml)
}

codemirror.vue:

import useLanguage from '@/.../language'
...
// Create your extension array and register it on codemirror
const extensions = [useLanguage()]