Repository: marimo-team/marimo
Pull request: improve: Hide code by default for Markdown cells
## 📝 Summary

Create Markdown cells on the frontend with `hide_code` set to `true` by default. Closes #2389.

## 🔍 Description of Changes

With @akshayka's change in #2504, this makes it so that the code editor for a Markdown cell is shown when editing, but hidden when the cell is not focused.

## 📋 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://discord.gg/JE7nhX6mD8), 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.


Problem statement:
Hide markdown duplication in 'edit' view.
### Description

I did a search but surprisingly could not see an existing issue for this. When editing a notebook, it would be nice to not see all of the markdown written out twice.

(This is kind of detail that makes Marimo a hard sell to my coworkers.)

I appreciate that you're getting bombarded with a lot of options for when to show/hide each individual widget in each mode. I'm sorry to be adding to that! :D

### Suggested solution

A Jupyter-like solution in which the display switches between the underlying markdown and the rendered HTML, when you click on it to edit it.

I appreciate that Marimo is a bit unusual in that it has *three* representations of Markdown (the underlying Python), which may complicate this further. I think the underlying Python is kind of an implementation detail of the serialization format though, maybe no-one ever needs to worry about that...

### Alternative

_No response_

### Additional context

_No response_

Hints from issue discussion:
We've talked about making the "markdown cells" WYSIWYG (like Colab for example), but it hasn't been a priority.

I'm surprised that the duplication makes it a hard sell, but that's good to know — thanks for letting us know.

>  I think the underlying Python is kind of an implementation detail of the serialization format though, maybe no-one ever needs to worry about that

The Python API of `mo.md()` is required for interpolating Python values (which requires using `mo.md(f"")`, including markdown in layouts, and conditionally writing markdown, all of which are not uncommon use cases. But you're totally right for pure markdown, `mo.md` is boilerplate.

Commit message: fix: Revert change in #2508 for testing

Patch to review:
diff --git a/frontend/src/components/editor/cell/code/cell-editor.tsx b/frontend/src/components/editor/cell/code/cell-editor.tsx
index 434848cdfe2..0247b8ccd8d 100644
--- a/frontend/src/components/editor/cell/code/cell-editor.tsx
+++ b/frontend/src/components/editor/cell/code/cell-editor.tsx
@@ -37,6 +37,7 @@ import { autoInstantiateAtom, isAiEnabled } from "@/core/config/config";
 import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
 import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
 import { useSplitCellCallback } from "../useSplitCell";
+import { isMarkdown } from "@/core/cells/utils";
 
 export interface CellEditorProps
   extends Pick<CellRuntimeState, "status">,
@@ -335,17 +336,26 @@ const CellEditorInternal = ({
     };
   }, [editorViewRef]);
 
-  const temporarilyShowCode = async () => {
+  const temporarilyShowCode = useCallback(async () => {
     if (hidden) {
       updateCellConfig({ cellId, config: { hide_code: false } });
       editorViewRef.current?.focus();
-      editorViewParentRef.current?.addEventListener(
+      editorViewRef.current?.contentDOM.addEventListener(
         "blur",
         () => updateCellConfig({ cellId, config: { hide_code: true } }),
         { once: true },
       );
     }
-  };
+  }, [hidden, cellId, updateCellConfig, editorViewRef]);
+
+  // For a newly created Markdown cell, which defaults to
+  // hidden code, we temporarily show the code editor
+  useEffect(() => {
+    if (!(isMarkdown(code) && code === `mo.md(r"""\n""")`)) {
+      return;
+    }
+    temporarilyShowCode();
+  }, [code, temporarilyShowCode]);
 
   return (
     <AiCompletionEditor
diff --git a/frontend/src/core/cells/__tests__/cells.test.ts b/frontend/src/core/cells/__tests__/cells.test.ts
index 50850a92670..aa5ff105aaa 100644
--- a/frontend/src/core/cells/__tests__/cells.test.ts
+++ b/frontend/src/core/cells/__tests__/cells.test.ts
@@ -1279,6 +1279,16 @@ describe("cell reducer", () => {
     expect(cell.consoleOutputs).toEqual([STDOUT1, STDOUT2]);
   });
 
+  it("can create markdown cell with code hidden by default", () => {
+    actions.createNewCell({
+      cellId: firstCellId,
+      before: false,
+      code: `mo.md(r"""Hello world!""")`,
+    });
+    const cellId = state.cellIds.atOrThrow(1);
+    expect(state.cellData[cellId].config.hide_code).toBe(true);
+  });
+
   describe("can create a cell using selected code", () => {
     const firstLine = "import marimo as mo";
     const secondLine = "import pandas as pd";
diff --git a/frontend/src/core/cells/cells.ts b/frontend/src/core/cells/cells.ts
index 1e887f996ae..8cadfbf64a7 100644
--- a/frontend/src/core/cells/cells.ts
+++ b/frontend/src/core/cells/cells.ts
@@ -29,7 +29,11 @@ import { deserializeBase64, deserializeJson } from "@/utils/json/base64";
 import { historyField } from "@codemirror/commands";
 import { clamp } from "@/utils/math";
 import type { LayoutState } from "../layout/layout";
-import { notebookIsRunning, notebookQueueOrRunningCount } from "./utils";
+import {
+  isMarkdown,
+  notebookIsRunning,
+  notebookQueueOrRunningCount,
+} from "./utils";
 import {
   extractHighlightedCode,
   splitEditor,
@@ -252,6 +256,10 @@ const {
           lastCodeRun,
           lastExecutionTime,
           edited: Boolean(newCellCode) && newCellCode !== lastCodeRun,
+          config: {
+            disabled: false,
+            hide_code: isMarkdown(newCellCode),
+          },
         }),
       },
       cellRuntime: {
diff --git a/frontend/src/core/cells/utils.ts b/frontend/src/core/cells/utils.ts
index 727c1894e4e..2b1fafc3e71 100644
--- a/frontend/src/core/cells/utils.ts
+++ b/frontend/src/core/cells/utils.ts
@@ -10,6 +10,7 @@ import {
   type LastSavedNotebook,
   staleCellIds,
 } from "./cells";
+import { LanguageAdapters } from "../codemirror/language/LanguageAdapters";
 
 export function notebookIsRunning(state: NotebookState) {
   return Object.values(state.cellRuntime).some(
@@ -102,3 +103,9 @@ export function getDescendantsStatus(state: NotebookState, cellId: CellId) {
     runningOrQueued,
   };
 }
+
+export function isMarkdown(code: string | undefined) {
+  return code && code !== ""
+    ? LanguageAdapters.markdown().isSupported(code)
+    : false;
+}


Code review comments:
diff_hunk: @@ -335,17 +336,26 @@ const CellEditorInternal = ({
     };
   }, [editorViewRef]);
 
-  const temporarilyShowCode = async () => {
+  const temporarilyShowCode = useCallback(async () => {
     if (hidden) {
       updateCellConfig({ cellId, config: { hide_code: false } });
       editorViewRef.current?.focus();
-      editorViewParentRef.current?.addEventListener(
+      editorViewRef.current?.contentDOM.addEventListener(
path: frontend/src/components/editor/cell/code/cell-editor.tsx
text: @user1:
Hmm, looks like my fix in #2508 got reverted. Is this intentional?

@user1:
Following up with a fix, `blur -> focusout`

@author:
Yes sorry, I left a comment on that PR but forgot to put one here. And thanks, that did the trick!

@author:
Well actually, it looks like you can't delete cells with the button again... Should we just move it somewhere else?