Folding all levels of code

Hey Marijn, great project you have here. I am trying to create a button that will fold all code levels. The core of my function is here:

function foldAllRecursive(view: EditorView) {
  // Traverse the syntax tree and collect all foldable ranges
  const foldRanges: { from: number, to: number }[] = [];
  syntaxTree(view.state).iterate({
    enter(node) {
      const from = node.from;
      const to = node.to;
      if (foldable(view.state, from, to)) {
        foldRanges.push({ from, to });
      }
    }
  });

  view.dispatch({
    effects: foldRanges.map(range => foldEffect.of({ from: range.from, to: range.to }))
  });
}

When I run this, it finds a bunch of lines that it can fold, it changes the gutter from \/ to >, and it adds ellipses to the page, but the content of the page is not folded.

If I use foldAll(view) then it does modify the page content as expected, but this seems to only fold the top-level code. I want to fold everything and then unfold it as necessary.

The full code for a basic example is below. I’m still fairly new to webdev, I tried following this example and I feel like I’m really close but perhaps missing something basic?

import { python } from '@codemirror/lang-python';
import { EditorState } from '@codemirror/state';
import { useEffect, useRef } from 'react';
import {
  EditorView,
  lineNumbers,
} from '@codemirror/view';
import {
  codeFolding,
  foldGutter,
  unfoldAll,
  syntaxTree,
  foldable,
  foldEffect,
  foldAll,
} from '@codemirror/language';

export const useCodeMirror = (userOptions: UserOptions) => {
  const mainViewRef = useRef<EditorView | null>(null);
  const mainStateRef = useRef<EditorState | null>(null);

  // Create main state
  useEffect(() => {
    if (!mainStateRef.current) {
      mainStateRef.current = EditorState.create({
        doc: userOptions.workbenchCode,
        extensions: [
          python(),
          codeFolding(),
          lineNumbers(),
          foldGutter(),
        ],
      });
    }
  }, [userOptions]);

  // Create main view
  useEffect(() => {
    if (!mainViewRef.current && mainStateRef.current) {
      mainViewRef.current = new EditorView({
        state: mainStateRef.current,
        parent: document.getElementById('custom-editor') as HTMLElement,
      });
    }
  }, []);

  return mainViewRef;
};

export const handleFoldClick = (
  setIsFolded: React.Dispatch<React.SetStateAction<boolean>>,
  editorViewRef: React.MutableRefObject<EditorView | null>,
) => {
  setIsFolded(prevValue => {
    if (editorViewRef.current) {
      if (prevValue) {
        unfoldAll(editorViewRef.current);
      } else {
        // foldAll(editorViewRef.current)
        foldAllRecursive(editorViewRef.current);
      }
    } else {
      console.error('CodeMirror editor not found');
    }
    return !prevValue;
  });
};

// Function to fold all levels of code
function foldAllRecursive(view: EditorView) {
  // Traverse the syntax tree and collect all foldable ranges
  const foldRanges: { from: number, to: number }[] = [];
  syntaxTree(view.state).iterate({
    enter(node) {
      const from = node.from;
      const to = node.to;
      if (foldable(view.state, from, to)) {
        foldRanges.push({ from, to });
      }
    }
  });

  view.dispatch({
    effects: foldRanges.map(range => foldEffect.of({ from: range.from, to: range.to }))
  });
}

You’re pusing the extent of the node, not the range returned by foldable, onto your array of ranges.

Beautiful, thank you

For future people my final code is:

// Function to fold all levels of code
function foldAllRecursive(view: EditorView) {
  const state = view.state;

  // Traverse the syntax tree and collect all foldable ranges
  const foldRanges: { from: number, to: number }[] = [];
  syntaxTree(state).iterate({
    enter(node) {
      const isFoldable = foldable(state, node.from, node.to)
      if (isFoldable) {
        foldRanges.push({ from: isFoldable.from, to: isFoldable.to });
      }
    }
  });

  view.dispatch({
    effects: foldRanges.map(range => foldEffect.of({ from: range.from, to: range.to }))
  });
}