Repository: marimo-team/marimo
Pull request #4550: feat: add expand and collapse all columns to notebook actions menu
Author: login: bjoaquinc
Created: 2025-04-15T22:58:10Z | Merged: 2025-04-16T02:09:45Z

## 📝 Summary

<!--
Provide a concise summary of what this pull request is addressing.

If this PR fixes any issues, list them here by number (e.g., Fixes #123).
-->

Partially resolves #4163 
<img width="323" alt="Screenshot 2025-04-15 at 6 56 22 PM" src="https://github.com/user-attachments/assets/45a2ef2a-2351-4cff-a420-6d77b50901e3" />
Next PR will add this functionality to hotkeys.tsx

## 🔍 Description of Changes

<!--
Detail the specific changes made in this pull request. Explain the problem addressed and how it was resolved. If applicable, provide before and after comparisons, screenshots, or any relevant details to help reviewers understand the changes easily.
-->

Created collapseAllColumns and expandAllColumns hooks then added them to the notebook actions menu.

## 📋 Checklist

- [x] I have read the [contributor guidelines](https://github.com/marimo-team/marimo/blob/main/CONTRIBUTING.md).
- [x] For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on [Discord](https://marimo.io/discord?ref=pr), or the community [discussions](https://github.com/marimo-team/marimo/discussions) (Please provide a link if applicable).
- [ ] I have added tests for the changes made.
- [x] I have run the code and verified that it works as expected.

## 📜 Reviewers

<!--
Tag potential reviewers from the community or maintainers who might be interested in reviewing this pull request.

Your PR will be reviewed more quickly if you can figure out the right person to tag with @ -->

@mscolnick



Diff:
diff --git a/frontend/src/components/editor/actions/useNotebookActions.tsx b/frontend/src/components/editor/actions/useNotebookActions.tsx
index 156f6caf21e..938656dc15b 100644
--- a/frontend/src/components/editor/actions/useNotebookActions.tsx
+++ b/frontend/src/components/editor/actions/useNotebookActions.tsx
@@ -38,6 +38,8 @@ import {
   MessagesSquareIcon,
   YoutubeIcon,
   DiamondPlusIcon,
+  ChevronDownCircleIcon,
+  ChevronRightCircleIcon,
 } from "lucide-react";
 import { commandPaletteAtom } from "../controls/command-palette";
 import {
@@ -81,6 +83,7 @@ import { useRunAllCells } from "../cell/useRunCells";
 import { settingDialogAtom } from "@/components/app-config/state";
 import { AddDatabaseDialogContent } from "../database/add-database-form";
 import { useHideAllMarkdownCode } from "./useHideAllMarkdownCode";
+import { useSectionCollapse } from "./useSectionCollapse";
 import { Constants } from "@/core/constants";
 import { getFeatureFlag } from "@/core/config/feature-flag";
 
@@ -97,6 +100,7 @@ export function useNotebookActions() {
   const [viewState] = useAtom(viewStateAtom);
   const kioskMode = useAtomValue(kioskModeAtom);
   const hideAllMarkdownCode = useHideAllMarkdownCode();
+  const { collapseAllSections, expandAllSections } = useSectionCollapse();
 
   const {
     updateCellConfig,
@@ -375,6 +379,16 @@ export function useNotebookActions() {
       label: "Hide all markdown code",
       handle: hideAllMarkdownCode,
     },
+    {
+      icon: <ChevronRightCircleIcon size={14} strokeWidth={1.5} />,
+      label: "Collapse all sections",
+      handle: collapseAllSections,
+    },
+    {
+      icon: <ChevronDownCircleIcon size={14} strokeWidth={1.5} />,
+      label: "Expand all sections",
+      handle: expandAllSections,
+    },
     {
       icon: <DiamondPlusIcon size={14} strokeWidth={1.5} />,
       label: "Add setup cell",
diff --git a/frontend/src/components/editor/actions/useSectionCollapse.ts b/frontend/src/components/editor/actions/useSectionCollapse.ts
new file mode 100644
index 00000000000..09caf028ec2
--- /dev/null
+++ b/frontend/src/components/editor/actions/useSectionCollapse.ts
@@ -0,0 +1,38 @@
+/* Copyright 2024 Marimo. All rights reserved. */
+import { useCellActions } from "@/core/cells/cells";
+import { getNotebook } from "@/core/cells/cells";
+import { canCollapseOutline } from "@/core/dom/outline";
+import useEvent from "react-use-event-hook";
+
+/**
+ * Hooks to collapse and expand all sections in the notebook.
+ */
+
+export const useSectionCollapse = () => {
+  const { collapseCell, expandCell } = useCellActions();
+
+  const processAllSections = async (action: "collapse" | "expand") => {
+    const notebook = getNotebook();
+    const cellIds = notebook.cellIds.inOrderIds;
+
+    // Find all markdown cells that aren't already hidden
+    for (const cellId of cellIds) {
+      const outline = notebook.cellRuntime[cellId].outline;
+      // Check if the cell is a markdown cell with a TOC outline
+      if (!outline) {
+        continue;
+      }
+      // Check if the cell is a collapsible header
+      if (!canCollapseOutline(outline)) {
+        continue;
+      }
+      // Collapse or expand the cell based on the action
+      action === "collapse" ? collapseCell({ cellId }) : expandCell({ cellId });
+    }
+  };
+
+  return {
+    collapseAllSections: useEvent(() => processAllSections("collapse")),
+    expandAllSections: useEvent(() => processAllSections("expand")),
+  };
+};


Reviews:
user: login: mscolnick
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T00:52:40Z
user: login: mscolnick
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T00:53:41Z
user: login: mscolnick
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T00:54:53Z
user: login: mscolnick
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T01:33:50Z
user: login: mscolnick
body: great work!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T01:49:47Z

Review comments:
diff_hunk: @@ -375,6 +383,16 @@ export function useNotebookActions() {
       label: "Hide all markdown code",
       handle: hideAllMarkdownCode,
     },
+    {
+      icon: <ChevronRightCircleIcon size={14} strokeWidth={1.5} />,
+      label: "Collapse all columns",
path: frontend/src/components/editor/actions/useNotebookActions.tsx
user: login: mscolnick
body: can this be `Collapse/Expand all sections`? 

and `column` →  `section` in other places 
created_at: 2025-04-16T00:52:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,57 @@
+/* Copyright 2024 Marimo. All rights reserved. */
+import { useCellActions } from "@/core/cells/cells";
+import { getNotebook } from "@/core/cells/cells";
+import { useCallback } from "react";
+import { canCollapseOutline } from "@/core/dom/outline";
+
+/**
+ * Hooks to collapse and expand all columns in the notebook.
+ */
+
+export const useCollapseAllColumns = () => {
+  const { collapseCell } = useCellActions();
+
+  return useCallback(async () => {
+    const notebook = getNotebook();
+    const cellIds = notebook.cellIds.inOrderIds;
+
+    // Find all markdown cells that aren't already hidden
+    for (const cellId of cellIds) {
+      const outline = notebook.cellRuntime[cellId].outline;
+      // Check if the cell is a markdown cell with a TOC outline
+      if (!outline) {
+        continue;
+      }
+      // Check if the cell is a collapsible header
+      if (!canCollapseOutline(outline)) {
+        continue;
+      }
+      // Collapse the cell
+      collapseCell({ cellId });
+    }
+  }, []);
+};
+
+export const useExpandAllColumns = () => {
+  const { expandCell } = useCellActions();
+
+  return useCallback(async () => {
path: frontend/src/components/editor/actions/useColumnExpansion.ts
user: login: mscolnick
body: this `useCallback` may contain old state. to achieve what you want, we use `useEvent` - it takes no deps but is always the same function (but without stale deps)
created_at: 2025-04-16T00:53:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,57 @@
+/* Copyright 2024 Marimo. All rights reserved. */
+import { useCellActions } from "@/core/cells/cells";
+import { getNotebook } from "@/core/cells/cells";
+import { useCallback } from "react";
+import { canCollapseOutline } from "@/core/dom/outline";
+
+/**
+ * Hooks to collapse and expand all columns in the notebook.
+ */
+
+export const useCollapseAllColumns = () => {
+  const { collapseCell } = useCellActions();
+
+  return useCallback(async () => {
+    const notebook = getNotebook();
+    const cellIds = notebook.cellIds.inOrderIds;
+
+    // Find all markdown cells that aren't already hidden
+    for (const cellId of cellIds) {
+      const outline = notebook.cellRuntime[cellId].outline;
+      // Check if the cell is a markdown cell with a TOC outline
+      if (!outline) {
+        continue;
+      }
+      // Check if the cell is a collapsible header
+      if (!canCollapseOutline(outline)) {
+        continue;
+      }
+      // Collapse the cell
+      collapseCell({ cellId });
+    }
+  }, []);
+};
+
+export const useExpandAllColumns = () => {
+  const { expandCell } = useCellActions();
+
+  return useCallback(async () => {
+    const notebook = getNotebook();
+    const cellIds = notebook.cellIds.inOrderIds;
+
+    // Find all markdown cells that aren't already hidden
+    for (const cellId of cellIds) {
+      const outline = notebook.cellRuntime[cellId].outline;
+      // Check if the cell is a markdown cell with a TOC outline
+      if (!outline) {
+        continue;
+      }
+      // Check if the cell is a collapsible header
+      if (!canCollapseOutline(outline)) {
+        continue;
+      }
+      // Collapse the cell
+      expandCell({ cellId });
+    }
+  }, []);
+};
path: frontend/src/components/editor/actions/useColumnExpansion.ts
user: login: mscolnick
body: could we combine these hooks, into `const {expandAllSection,collapseAllSection} = useSectionCollapse()`
created_at: 2025-04-16T00:54:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+/* Copyright 2024 Marimo. All rights reserved. */
+import { useCellActions } from "@/core/cells/cells";
+import { getNotebook } from "@/core/cells/cells";
+import { canCollapseOutline } from "@/core/dom/outline";
+import useEvent from "react-use-event-hook";
+
+/**
+ * Hooks to collapse and expand all sections in the notebook.
+ */
+
+const useCollapseAllSections = () => {
+  const { collapseCell } = useCellActions();
+
+  return useEvent(async () => {
+    const notebook = getNotebook();
+    const cellIds = notebook.cellIds.inOrderIds;
+
+    // Find all markdown cells that aren't already hidden
+    for (const cellId of cellIds) {
+      const outline = notebook.cellRuntime[cellId].outline;
+      // Check if the cell is a markdown cell with a TOC outline
+      if (!outline) {
+        continue;
+      }
+      // Check if the cell is a collapsible header
+      if (!canCollapseOutline(outline)) {
+        continue;
+      }
+      // Collapse the cell
+      collapseCell({ cellId });
+    }
+  });
+};
+
+const useExpandAllSections = () => {
+  const { expandCell } = useCellActions();
+
+  return useEvent(async () => {
+    const notebook = getNotebook();
+    const cellIds = notebook.cellIds.inOrderIds;
+
+    // Find all markdown cells that aren't already hidden
+    for (const cellId of cellIds) {
+      const outline = notebook.cellRuntime[cellId].outline;
+      // Check if the cell is a markdown cell with a TOC outline
+      if (!outline) {
+        continue;
+      }
+      // Check if the cell is a collapsible header
+      if (!canCollapseOutline(outline)) {
+        continue;
+      }
+      // Collapse the cell
+      expandCell({ cellId });
+    }
+  });
+};
+
+// Consolidate the two hooks into one object
+export const useSectionCollapse = () => {
+  return {
+    collapseAllSections: useCollapseAllSections(),
+    expandAllSections: useExpandAllSections(),
+  };
+};
path: frontend/src/components/editor/actions/useSectionCollapse.ts
user: login: mscolnick
body: how about something like to DRY things up a bit:

```ts
export const useSectionCollapse = () => {
  const { collapseCell, expandCell } = useCellActions();

  const processAllSections = async (action: 'collapse' | 'expand') => {
    const notebook = getNotebook();
    const cellIds = notebook.cellIds.inOrderIds;

    // Find all markdown cells that aren't already hidden
    for (const cellId of cellIds) {
      const outline = notebook.cellRuntime[cellId].outline;
      // Check if the cell is a markdown cell with a TOC outline
      if (!outline) {
        continue;
      }
      // Check if the cell is a collapsible header
      if (!canCollapseOutline(outline)) {
        continue;
      }
        action === 'collapse' ? collapseCell({ cellId }) : expandCell({ cellId });
    }
  };

  return {
    collapseAllSections: useEvent(() => processAllSections('collapse')),
    expandAllSections: useEvent(() => processAllSections('expand'))
  };
};
```
created_at: 2025-04-16T01:33:50Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-15T22:58:14Z
body: [vc]: #G/iis5v1rGw+RRD2CW1Gkbbni7xhKjWsAVUFjr9h8LM=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJtYXJpbW8tc3Rvcnlib29rIiwicm9vdERpcmVjdG9yeSI6ImZyb250ZW5kIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL21hcmltby9tYXJpbW8tc3Rvcnlib29rLzl5N0gyZ3FGOThFYnJyZ01TWEdYVFhkUUhHSDQiLCJwcmV2aWV3VXJsIjoibWFyaW1vLXN0b3J5Ym9vay1naXQtZm9yay1iam9hcXVpbmMtbmV3LWtleWJvYXJkLWE1MTg3Ny1tYXJpbW8udmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJtYXJpbW8tc3Rvcnlib29rLWdpdC1mb3JrLWJqb2FxdWluYy1uZXcta2V5Ym9hcmQtYTUxODc3LW1hcmltby52ZXJjZWwuYXBwIn19LHsibmFtZSI6Im1hcmltby1kb2NzIiwicm9vdERpcmVjdG9yeSI6ImRvY3MiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbWFyaW1vL21hcmltby1kb2NzL0ZkWE01Skh5TlkzVmY5aEo4enkxWERaZVdCaWMiLCJwcmV2aWV3VXJsIjoibWFyaW1vLWRvY3MtZ2l0LWZvcmstYmpvYXF1aW5jLW5ldy1rZXlib2FyZC1zaG9ydGN1dHMtbWFyaW1vLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoibWFyaW1vLWRvY3MtZ2l0LWZvcmstYmpvYXF1aW5jLW5ldy1rZXlib2FyZC1zaG9ydGN1dHMtbWFyaW1vLnZlcmNlbC5hcHAifX1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **marimo-docs** | ✅ Ready ([Inspect](https://vercel.com/marimo/marimo-docs/FdXM5JHyNY3Vf9hJ8zy1XDZeWBic)) | [Visit Preview](https://marimo-docs-git-fork-bjoaquinc-new-keyboard-shortcuts-marimo.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/marimo-docs-git-fork-bjoaquinc-new-keyboard-shortcuts-marimo.vercel.app?via=pr-comment-feedback-link) | Apr 16, 2025 1:49am |
| **marimo-storybook** | ✅ Ready ([Inspect](https://vercel.com/marimo/marimo-storybook/9y7H2gqF98EbrrgMSXGXTXdQHGH4)) | [Visit Preview](https://marimo-storybook-git-fork-bjoaquinc-new-keyboard-a51877-marimo.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/marimo-storybook-git-fork-bjoaquinc-new-keyboard-a51877-marimo.vercel.app?via=pr-comment-feedback-link) | Apr 16, 2025 1:49am |



author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-15T22:58:20Z
body: All contributors have signed the CLA  ✍️ ✅<br/><sub>Posted by the ****CLA Assistant Lite bot****.</sub>
author_association: NONE
user: login: bjoaquinc
created_at: 2025-04-15T23:01:43Z
body: I have read the CLA Document and I hereby sign the CLA
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-16T02:18:45Z
body: 🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.12.10-dev21
author_association: NONE