Repository: marimo-team/marimo
Pull request: feat: enable toggling to md mode automatically via mod-shift-m
Features demonstrated in video:
- markdown toggle in cell menu
- markdown toggle doesnt show in cell menu when non-markdown-convertible code
- markdown shortcut works (mod-shift-m)

However, it seems that when a cell is created, the `editorView` is null until _something_ is typed. For this reason, I only know how to get this working after typing some whitespace character first, not sure how to actually initialize the EditorView.

Fixes #1003 

https://github.com/marimo-team/marimo/assets/33243383/4dca535f-7611-4594-9613-136213a2bfd7



Problem statement:
Markdown Cell Shortcut?
### Description

I have to type `mo.md()` first to start a markdown cell / get the markdown editor to show up (I think?)

### Suggested solution

Have either a shortcut (cmd+shift+m?) or UI method in a cell's six-dot menu to convert a cell to markdown instead, so I can immediately start text using the markdown editor

### Alternative

_No response_

### Additional context

_No response_

Hints from issue discussion:
> I have to type mo.md() first to start a markdown cell / get the markdown editor to show up (I think?)

Yes, that's right.

In marimo, there are only Python cells; we don't have a distinction between Python and Markdown cells. The markdown editor is just a friendly skin over `mo.md()`.

> Have either a shortcut (cmd+shift+m?) 

We'd welcome a PR adding a keyboard shortcut (potentially the one you suggest, if it plays well across browsers) that toggles the markdown view when possible -- this would work on empty cells, as well as cells starting with `mo.md`.

Commit message: feat: enable toggling to md mode automatically via mod-shift-m

Patch to review:
diff --git a/frontend/src/components/editor/actions/useCellActionButton.tsx b/frontend/src/components/editor/actions/useCellActionButton.tsx
index 39054da87fc..882daabacd8 100644
--- a/frontend/src/components/editor/actions/useCellActionButton.tsx
+++ b/frontend/src/components/editor/actions/useCellActionButton.tsx
@@ -2,7 +2,11 @@
 import { sendDeleteCell } from "@/core/network/requests";
 import { downloadCellOutput } from "@/components/export/export-output-button";
 import { Switch } from "@/components/ui/switch";
-import { formatEditorViews } from "@/core/codemirror/format";
+import {
+  canToggleMarkdown,
+  formatEditorViews,
+  toggleToMarkdown,
+} from "@/core/codemirror/format";
 import { useCellActions } from "@/core/cells/cells";
 import {
   ImageIcon,
@@ -39,6 +43,7 @@ import {
   DialogHeader,
 } from "@/components/ui/dialog";
 import { Label } from "@/components/ui/label";
+import { MarkdownIcon } from "../cell/code/icons";
 
 export interface CellActionButtonProps
   extends Pick<CellData, "name" | "config"> {
@@ -178,6 +183,18 @@ export function useCellActionButtons({ cell }: Props) {
           formatEditorViews({ [cellId]: editorView }, updateCellCode);
         },
       },
+      {
+        icon: <MarkdownIcon />,
+        label: "Markdown mode",
+        hotkey: "cell.markdownMode",
+        hidden: !canToggleMarkdown(editorView),
+        handle: () => {
+          if (!editorView) {
+            return;
+          }
+          toggleToMarkdown(cellId, editorView, updateCellCode);
+        },
+      },
       {
         icon: config.hide_code ? (
           <EyeIcon size={13} strokeWidth={1.5} />
diff --git a/frontend/src/core/codemirror/extensions.ts b/frontend/src/core/codemirror/extensions.ts
index 3b9c1fcf86d..600d113e622 100644
--- a/frontend/src/core/codemirror/extensions.ts
+++ b/frontend/src/core/codemirror/extensions.ts
@@ -1,7 +1,7 @@
 /* Copyright 2024 Marimo. All rights reserved. */
 import { EditorView, keymap, placeholder } from "@codemirror/view";
 import { CellId } from "../cells/ids";
-import { formatEditorViews } from "./format";
+import { formatEditorViews, toggleToMarkdown } from "./format";
 import { smartScrollIntoView } from "../../utils/scroll";
 import { HOTKEYS } from "@/core/hotkeys/hotkeys";
 import { CellActions } from "../cells/cells";
@@ -67,6 +67,14 @@ export function formatKeymapExtension(
         return true;
       },
     },
+    {
+      key: HOTKEYS.getHotkey("cell.markdownMode").key,
+      preventDefault: true,
+      run: (ev) => {
+        toggleToMarkdown(cellId, ev, updateCellCode);
+        return true;
+      },
+    },
   ]);
 }
 
diff --git a/frontend/src/core/codemirror/format.ts b/frontend/src/core/codemirror/format.ts
index 31c3553695e..ff7838bdaa6 100644
--- a/frontend/src/core/codemirror/format.ts
+++ b/frontend/src/core/codemirror/format.ts
@@ -14,6 +14,11 @@ import {
 } from "./language/utils";
 import { StateEffect } from "@codemirror/state";
 import { getUserConfig } from "../config/config";
+import {
+  LanguageAdapters,
+  languageAdapterState,
+  switchLanguage,
+} from "./language/extension";
 
 export const formattingChangeEffect = StateEffect.define<boolean>();
 
@@ -58,3 +63,37 @@ export function formatAll(updateCellCode: CellActions["updateCellCode"]) {
   const views = notebookCellEditorViews(getNotebook());
   return formatEditorViews(views, updateCellCode);
 }
+
+export function canToggleMarkdown(editorView: EditorView | null) {
+  if (
+    !editorView ||
+    editorView.state.field(languageAdapterState).type === "markdown"
+  ) {
+    return false;
+  }
+  return (
+    LanguageAdapters.markdown().isSupported(
+      getEditorCodeAsPython(editorView),
+    ) || getEditorCodeAsPython(editorView).trim() === ""
+  );
+}
+
+export function toggleToMarkdown(
+  cellId: CellId,
+  editorView: EditorView,
+  updateCellCode: CellActions["updateCellCode"],
+) {
+  if (!canToggleMarkdown(editorView)) {
+    return;
+  }
+  if (getEditorCodeAsPython(editorView).trim() === "") {
+    const blankMd = 'mo.md("")';
+    updateCellCode({
+      cellId,
+      code: blankMd,
+      formattingChange: true,
+    });
+    updateEditorCodeFromPython(editorView, blankMd);
+  }
+  switchLanguage(editorView, "markdown");
+}
diff --git a/frontend/src/core/hotkeys/hotkeys.ts b/frontend/src/core/hotkeys/hotkeys.ts
index 0fa52c54cd6..7e9764366b6 100644
--- a/frontend/src/core/hotkeys/hotkeys.ts
+++ b/frontend/src/core/hotkeys/hotkeys.ts
@@ -104,6 +104,11 @@ const DEFAULT_HOT_KEY = {
     group: "Editing",
     key: "Mod-b",
   },
+  "cell.markdownMode": {
+    name: "Markdown mode",
+    group: "Editing",
+    key: "Mod-Shift-m",
+  },
   "cell.complete": {
     name: "Code completion",
     group: "Editing",


Code review comments:
diff_hunk: @@ -58,3 +63,37 @@ export function formatAll(updateCellCode: CellActions["updateCellCode"]) {
   const views = notebookCellEditorViews(getNotebook());
   return formatEditorViews(views, updateCellCode);
 }
+
+export function canToggleMarkdown(editorView: EditorView | null) {
+  if (
+    !editorView ||
+    editorView.state.field(languageAdapterState).type === "markdown"
+  ) {
+    return false;
+  }
+  return (
+    LanguageAdapters.markdown().isSupported(
+      getEditorCodeAsPython(editorView),
+    ) || getEditorCodeAsPython(editorView).trim() === ""
+  );
+}
+
+export function toggleToMarkdown(
+  cellId: CellId,
+  editorView: EditorView,
+  updateCellCode: CellActions["updateCellCode"],
+) {
+  if (!canToggleMarkdown(editorView)) {
+    return;
+  }
+  if (getEditorCodeAsPython(editorView).trim() === "") {
+    const blankMd = 'mo.md("")';
path: frontend/src/core/codemirror/format.ts
text: @user1:
What do you think about defaulting to a raw f-string? That will make it possible to interpolate Python values with `{}`, and make it easier to write LaTex. ie

```
mo.md(
    rf"""
    """
)
```

@author:
Done, I don't think the triple quotes is required since it seems to get removed to `rf""` whenever I switch back and forth, but adding actual python seems to automatically trigger going to triple quotes?

diff_hunk: @@ -178,6 +183,18 @@ export function useCellActionButtons({ cell }: Props) {
           formatEditorViews({ [cellId]: editorView }, updateCellCode);
         },
       },
+      {
+        icon: <MarkdownIcon />,
+        label: "Markdown mode",
path: frontend/src/components/editor/actions/useCellActionButton.tsx
text: @user1:
```suggestion
        label: "View as Markdown",
```

@author:
Done I think -- used the same format for the `View as Python` when toggling back, lmk if you have a diff preference

diff_hunk: @@ -104,6 +104,11 @@ const DEFAULT_HOT_KEY = {
     group: "Editing",
     key: "Mod-b",
   },
+  "cell.markdownMode": {
path: frontend/src/core/hotkeys/hotkeys.ts
text: @user1:
Instead of only switching to "markdown mode", how about using this hotkey to toggle the markdown view on and off? i.e., To switch back and forth between markdown view and Python view?

@author:
yes! done