How can I customize the fold widget based on the contents of the folded text?

I’m trying to create a custom folding extension where the fold widget depends on the contents of the folded text, but I can’t find an obvious way to capture it. My strategy was to call foldGutter from @codemirror/language and replace its returned contents with my own replacement to codeFolding() Here’s my current code:

export function customFold(): Extension {
  function customCodeFolding(): Extension {
    return codeFolding({
      placeholderDOM: (view, onclick) => {
        let element = document.createElement("span")
        element.textContent = "????"
        element.setAttribute("aria-label", "folded code")
        element.title = "unfold"
        element.className = "cm-foldPlaceholder"
        element.onclick = onclick
        return element
      }
    })
  }

  const foldGutterExtension = foldGutter() as Extension[];
  foldGutterExtension.pop()
  foldGutterExtension.push(customCodeFolding());
  return foldGutterExtension;
}

My goal is to replace the ???? with some other content computed from the folded text. Is there an easy way to do it?

See this thread.

1 Like

This works very well, thanks!

Would it be possible to leverage Codemirror existing syntax highlighting infrastructure to highlight the collapsed text (using the same color scheme as the main editor)?

To elaborate: I’m using Codemirror to implement a JSON viewer (similar to GitHub - mac-s-g/react-json-view: JSON viewer for react, but all inside codemirror). the JSON contained in the editor is always expanded, but with folding and the preparePlaceholder it collapses in a single line. Only downside of the current approach is that I lost JSON syntax highlighting when collapsed. Here’s the updated code BTW:

export function jsonFold(): Extension {
  function jsonCodeFolding(): Extension {
    return codeFolding({
      preparePlaceholder: (state, range) => {
        const text = state.sliceDoc(range.from - 1, range.to + 1)
        return JSON.stringify(JSON.parse(text)).slice(1, -1)
      },
      placeholderDOM: (_view, onclick, prepared) => {
        let element = document.createElement("span")
        element.textContent = prepared
        element.setAttribute("aria-label", "folded code")
        element.title = "unfold"
        // element.className = "cm-foldPlaceholder"
        element.onclick = onclick
        return element
      }
    })
  }

  const foldGutterExtension = foldGutter() as Extension[];
  foldGutterExtension.pop()
  foldGutterExtension.push(jsonCodeFolding());
  return foldGutterExtension;
}

Are you displaying the full folded text as a fold marker? What is the point of folding, then?

The difference is that the folded text is not pretty printed, just a simple JSON.stringify(obj) and thus fits in a single line. The editor value is being set as items.map(v => JSON.stringify(v, null, ' ')).join('\n') (we prettify each object before adding to the editor), so each object can span multiple lines.

You should be able to use jsonLanguage.parse along with highlightTree and your highlighting style to get the same type of highlighting on the DOM generated by your code.

1 Like

@marijn thanks a lot for the help so far. I managed to get the fold summary highlighted with the following code which generates the innerHTML of the placeholderDOM element:

function highlight(jsonStr: string): string {
  const tree = jsonLanguage.parser.parse(jsonStr);
  const chunks = [] as string[];
  let pos = 0;
  highlightTree(tree, defaultHighlightStyle, (from, to, classes) => {
    chunks.push(jsonStr.slice(pos, from));
    chunks.push(`<span class="${classes}">${jsonStr.slice(from, to)}</span>`);
    pos = to;
  });
  chunks.push(jsonStr.slice(pos));
  const result = chunks.join('');
  return result;
}

This seems to work, but it uses the default highlighting theme. Can you give me a hint on how do I use my own custom there with highlightTree? I’ve looked around the type definitions, but couldn’t find how to obtain a HighlightStyle from the theme I’m using (which is an Extension).

You need the HighlightStyle to be able to use it. Change the theme (or ask the maintainers) to export that instead of an extension.

1 Like

Thanks for the follow up. I’m using vscode-dark (one of react-codemirror themes), and was able to extract the HighlightStyle with the following hacky workaround:

((vsCodeDark as unknown as Array<Extension[]>)[1][2] as any)
  .value as HighlightStyle;

While this works, it would be nice if there was an explicit way to extract the HighlightStyle for the currently installed theme. Maybe my use case is very specific, but there could be scenarios where one could want to apply the current theme’s highlighting to a custom widget.

That’s terrible and not very type-safe—Extension values are opaque in this system, you can’t really inspect or unpack. The idea is that packages providing them also provide any primitives that client code might want to use as separate exports.