@marimo-team/islands 0.23.14-dev16 → 0.23.14-dev19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
3
- import { at as parseHtmlContent, it as ansiToPlainText } from "./html-to-image-DcTkcjmm.js";
3
+ import { at as parseHtmlContent, it as ansiToPlainText } from "./html-to-image-DBHfshED.js";
4
4
  import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
5
5
  import { t as Strings } from "./strings-Dq_j3Rxw.js";
6
6
  import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
@@ -6,10 +6,10 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
6
6
  import { _ as Logger, g as cn, h as Events, l as useEventListener, t as Button } from "./button-BacYv-bE.js";
7
7
  import { t as require_react } from "./react-DA-nE2FX.js";
8
8
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
9
- import { lt as kioskModeAtom } from "./html-to-image-DcTkcjmm.js";
9
+ import { lt as kioskModeAtom } from "./html-to-image-DBHfshED.js";
10
10
  import "./chunk-5FQGJX7Z-BbqSm5gU.js";
11
11
  import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
12
- import { a as DEFAULT_SLIDE_TYPE, an as EyeOff, c as Slide, ct as PanelResizeHandle, i as DEFAULT_DECK_TRANSITION, ln as Code, on as Expand, ot as Panel, s as SlideSidebar, st as PanelGroup, t as useNotebookCodeAvailable } from "./code-visibility-Dc1Ap0mZ.js";
12
+ import { a as DEFAULT_SLIDE_TYPE, c as Slide, ct as PanelGroup, i as DEFAULT_DECK_TRANSITION, lt as PanelResizeHandle, on as EyeOff, s as SlideSidebar, sn as Expand, st as Panel, t as useNotebookCodeAvailable, un as Code } from "./code-visibility-CxDvzm2S.js";
13
13
  import { X as useDebouncedCallback } from "./input-BSdZp5Ng.js";
14
14
  import "./toDate-D1Z7ZXWh.js";
15
15
  import "./react-dom-BTJzcVJ9.js";
@@ -7245,12 +7245,12 @@ function _temp(e5) {
7245
7245
  };
7246
7246
  }
7247
7247
  var SubslideView = (e5) => {
7248
- let t = (0, import_compiler_runtime.c)(24), { subslide: n, resolveShowCode: r, isEditable: i, slideConfigs: a } = e5, o, s, c, l, u, f;
7248
+ let t = (0, import_compiler_runtime.c)(24), { subslide: n, resolveShowCode: r, isEditable: i, slideConfigs: a } = e5, o, s, c, l, u, ro;
7249
7249
  if (t[0] !== i || t[1] !== r || t[2] !== a || t[3] !== n) {
7250
7250
  let { slideLevel: e6, cumulativeByBlock: io2 } = buildSubslideNotes(n, a);
7251
7251
  s = e6;
7252
7252
  let p2 = n.blocks.some((e7) => e7.cells.some((e8) => r(e8.id)));
7253
- o = w, f = "h-full w-full overflow-auto flex", c = p2 ? "mo-slide-content flex flex-col gap-3" : "mo-slide-content", t[10] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (l = { margin: "auto 20px" }, t[10] = l) : l = t[10], u = n.blocks.map((e7, t2) => {
7253
+ o = w, ro = "h-full w-full overflow-auto flex", c = p2 ? "mo-slide-content flex flex-col gap-3" : "mo-slide-content", t[10] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (l = { margin: "auto 20px" }, t[10] = l) : l = t[10], u = n.blocks.map((e7, t2) => {
7254
7254
  let n2 = e7.cells.map((e8) => r(e8.id) ? i ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlideCellView, { cell: e8 }, e8.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlideCellReadOnlyView, { cell: e8 }, e8.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slide, {
7255
7255
  cellId: e8.id,
7256
7256
  status: e8.status,
@@ -7264,8 +7264,8 @@ var SubslideView = (e5) => {
7264
7264
  }, t2);
7265
7265
  }
7266
7266
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: n2 }, t2);
7267
- }), t[0] = i, t[1] = r, t[2] = a, t[3] = n, t[4] = o, t[5] = s, t[6] = c, t[7] = l, t[8] = u, t[9] = f;
7268
- } else o = t[4], s = t[5], c = t[6], l = t[7], u = t[8], f = t[9];
7267
+ }), t[0] = i, t[1] = r, t[2] = a, t[3] = n, t[4] = o, t[5] = s, t[6] = c, t[7] = l, t[8] = u, t[9] = ro;
7268
+ } else o = t[4], s = t[5], c = t[6], l = t[7], u = t[8], ro = t[9];
7269
7269
  let io;
7270
7270
  t[11] !== c || t[12] !== l || t[13] !== u ? (io = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
7271
7271
  className: c,
@@ -7273,10 +7273,10 @@ var SubslideView = (e5) => {
7273
7273
  children: u
7274
7274
  }), t[11] = c, t[12] = l, t[13] = u, t[14] = io) : io = t[14];
7275
7275
  let p;
7276
- t[15] !== f || t[16] !== io ? (p = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
7277
- className: f,
7276
+ t[15] !== ro || t[16] !== io ? (p = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
7277
+ className: ro,
7278
7278
  children: io
7279
- }), t[15] = f, t[16] = io, t[17] = p) : p = t[17];
7279
+ }), t[15] = ro, t[16] = io, t[17] = p) : p = t[17];
7280
7280
  let ao;
7281
7281
  t[18] === s ? ao = t[19] : (ao = s && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NotesAside, { text: s }), t[18] = s, t[19] = ao);
7282
7282
  let oo;
@@ -7302,7 +7302,7 @@ var ParkedPreviewContent = (e5) => {
7302
7302
  status: n.status,
7303
7303
  output: n.output
7304
7304
  }), t[3] = n.id, t[4] = n.output, t[5] = n.status, t[6] = o) : o = t[6], o;
7305
- }, reveal_component_default = ({ slideCells: e5, layout: o, setLayout: s, noOutputIds: l, activeIndex: u, onSlideChange: ro, mode: p, configWidth: fo, isEditable: po = false }) => {
7305
+ }, reveal_component_default = ({ slideCells: e5, layout: o, setLayout: s, noOutputIds: l, activeIndex: u, onSlideChange: f, mode: io, configWidth: fo, isEditable: po = false }) => {
7306
7306
  var _a2, _b2;
7307
7307
  let yo = (0, import_react.useRef)(null), bo = (0, import_react.useRef)(null), { width: So, height: Co } = useSlideDimensions(yo), wo = useFullScreenElement() != null, To = useAtomValue(kioskModeAtom), Eo = (0, import_react.useMemo)(() => To ? [] : [ce], [To]), [Do, Oo] = (0, import_react.useState)(() => /* @__PURE__ */ new Set()), ko = useNotebookCodeAvailable(e5), Ao = !isIslands() && ko, jo = u == null ? void 0 : e5[u], Mo = jo ?? e5.at(0), { parkedPreviewCell: No, isHeldEdit: Po, isNoOutputPreview: Fo, heldEditCellId: Io, heldShowsCode: Lo, toggleHeldShowsCode: Ro } = useParkedPreview({
7308
7308
  activeCell: jo,
@@ -7391,14 +7391,14 @@ var ParkedPreviewContent = (e5) => {
7391
7391
  let e6 = bo.current;
7392
7392
  if (!e6) return;
7393
7393
  let t = resolveActiveCellIndex(Go, e6.getIndices());
7394
- t != null && (ro == null ? void 0 : ro(t));
7394
+ t != null && (f == null ? void 0 : f(t));
7395
7395
  }), $o = useEvent_default((t) => {
7396
7396
  if (!No || u == null || Events.fromInput(t)) return;
7397
7397
  let n = classifyNavKey(t);
7398
7398
  if (n === 0) return;
7399
7399
  t.preventDefault(), t.stopPropagation();
7400
7400
  let i = u + n;
7401
- i < 0 || i >= e5.length || (ro == null ? void 0 : ro(i));
7401
+ i < 0 || i >= e5.length || (f == null ? void 0 : f(i));
7402
7402
  }), ns = useEvent_default(() => {
7403
7403
  Qo(), triggerResize(bo.current);
7404
7404
  });
@@ -7494,7 +7494,7 @@ var ParkedPreviewContent = (e5) => {
7494
7494
  ]
7495
7495
  })
7496
7496
  });
7497
- return p === "read" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
7497
+ return io === "read" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
7498
7498
  className: "flex-1 min-w-0 flex flex-row gap-3",
7499
7499
  children: os
7500
7500
  }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.14-dev16",
3
+ "version": "0.23.14-dev19",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -126,3 +126,28 @@ describe("ColumnExplorerPanel header counts", () => {
126
126
  expect(screen.queryByText(/hidden/)).not.toBeInTheDocument();
127
127
  });
128
128
  });
129
+
130
+ describe("ColumnExplorerPanel visibility actions", () => {
131
+ const showAll = () => screen.getByRole("button", { name: /Show all/ });
132
+ const hideAll = () => screen.getByRole("button", { name: /Hide all/ });
133
+
134
+ it("disables 'Show all' when every column is visible", () => {
135
+ renderPanel();
136
+ expect(showAll()).toBeDisabled();
137
+ expect(hideAll()).toBeEnabled();
138
+ });
139
+
140
+ it("disables 'Hide all' when every column is hidden", () => {
141
+ renderPanel({
142
+ initiallyHidden: ["customer_name", "cust_age", "order_total"],
143
+ });
144
+ expect(showAll()).toBeEnabled();
145
+ expect(hideAll()).toBeDisabled();
146
+ });
147
+
148
+ it("enables both actions when some columns are hidden", () => {
149
+ renderPanel({ initiallyHidden: ["cust_age"] });
150
+ expect(showAll()).toBeEnabled();
151
+ expect(hideAll()).toBeEnabled();
152
+ });
153
+ });
@@ -21,10 +21,17 @@ beforeAll(() => {
21
21
 
22
22
  type Row = Record<string, unknown>;
23
23
 
24
+ // Select, index, and nameless columns are non-hideable in production
25
+ // (see columns.tsx), so the harness mirrors that to keep visibility counts
26
+ // aligned with the real table.
24
27
  const TEST_COLUMNS: ColumnDef<Row>[] = [
25
- { id: SELECT_COLUMN_ID, accessorKey: SELECT_COLUMN_ID },
26
- { id: INDEX_COLUMN_NAME, accessorKey: INDEX_COLUMN_NAME },
27
- { id: "__m_column__0", accessorKey: "__m_column__0" },
28
+ { id: SELECT_COLUMN_ID, accessorKey: SELECT_COLUMN_ID, enableHiding: false },
29
+ {
30
+ id: INDEX_COLUMN_NAME,
31
+ accessorKey: INDEX_COLUMN_NAME,
32
+ enableHiding: false,
33
+ },
34
+ { id: "__m_column__0", accessorKey: "__m_column__0", enableHiding: false },
28
35
  {
29
36
  id: "customer_name",
30
37
  accessorKey: "customer_name",
@@ -101,6 +108,7 @@ describe("ColumnVisibilityDropdown", () => {
101
108
  renderAndOpen({ initiallyHidden: ["cust_age"] });
102
109
  expect(getOptionTexts()).toEqual([
103
110
  "Show all",
111
+ "Hide all",
104
112
  "cust_age",
105
113
  "customer_name",
106
114
  "order_total",
@@ -117,6 +125,7 @@ describe("ColumnVisibilityDropdown", () => {
117
125
 
118
126
  expect(getOptionTexts()).toEqual([
119
127
  "Show all",
128
+ "Hide all",
120
129
  "cust_age",
121
130
  "customer_name",
122
131
  "order_total",
@@ -132,6 +141,7 @@ describe("ColumnVisibilityDropdown", () => {
132
141
  fireEvent.click(getColumnOption("customer_name"));
133
142
  expect(getOptionTexts()).toEqual([
134
143
  "Show all",
144
+ "Hide all",
135
145
  "cust_age",
136
146
  "customer_name",
137
147
  "order_total",
@@ -144,6 +154,7 @@ describe("ColumnVisibilityDropdown", () => {
144
154
  // Reopen sorts both hidden columns first, preserving table order.
145
155
  expect(getOptionTexts()).toEqual([
146
156
  "Show all",
157
+ "Hide all",
147
158
  "customer_name",
148
159
  "cust_age",
149
160
  "order_total",
@@ -189,6 +200,52 @@ describe("ColumnVisibilityDropdown", () => {
189
200
  );
190
201
  });
191
202
 
203
+ it("'Hide all' hides every hideable column", () => {
204
+ renderAndOpen();
205
+ fireEvent.click(getColumnOption("Hide all"));
206
+
207
+ expect(
208
+ getColumnOption("customer_name").querySelector(".lucide-eye-off"),
209
+ ).not.toBeNull();
210
+ expect(
211
+ getColumnOption("cust_age").querySelector(".lucide-eye-off"),
212
+ ).not.toBeNull();
213
+ expect(
214
+ getColumnOption("order_total").querySelector(".lucide-eye-off"),
215
+ ).not.toBeNull();
216
+ expect(getColumnOption("Hide all")).toHaveAttribute(
217
+ "aria-disabled",
218
+ "true",
219
+ );
220
+ });
221
+
222
+ it("disables 'Hide all' when every column is already hidden", () => {
223
+ renderAndOpen({
224
+ initiallyHidden: ["customer_name", "cust_age", "order_total"],
225
+ });
226
+ expect(getColumnOption("Hide all")).toHaveAttribute(
227
+ "aria-disabled",
228
+ "true",
229
+ );
230
+ });
231
+
232
+ it("'Hide all' leaves non-hideable columns visible", () => {
233
+ renderAndOpen({ nonHideable: ["customer_name"] });
234
+ fireEvent.click(getColumnOption("Hide all"));
235
+
236
+ expect(
237
+ getColumnOption("cust_age").querySelector(".lucide-eye-off"),
238
+ ).not.toBeNull();
239
+ const nonHideable = getColumnOption("customer_name");
240
+ expect(nonHideable).toHaveAttribute("aria-disabled", "true");
241
+ expect(nonHideable.querySelector(".lucide-eye-off")).toBeNull();
242
+ // Every hideable column is now hidden, so the action gates off.
243
+ expect(getColumnOption("Hide all")).toHaveAttribute(
244
+ "aria-disabled",
245
+ "true",
246
+ );
247
+ });
248
+
192
249
  it("hides and shows matching columns via bulk actions while searching", () => {
193
250
  renderAndOpen();
194
251
  fireEvent.change(getSearchInput(), { target: { value: "cust" } });
@@ -50,7 +50,10 @@ import {
50
50
  import { smartMatch } from "@/utils/smartMatch";
51
51
  import type { Column, Table } from "@tanstack/react-table";
52
52
  import { cn } from "@/utils/cn";
53
- import { getColumnCountForDisplay } from "../hooks/use-column-visibility";
53
+ import {
54
+ getColumnCountForDisplay,
55
+ getUserColumnVisibilityCounts,
56
+ } from "../hooks/use-column-visibility";
54
57
 
55
58
  interface ColumnExplorerPanelProps<TData> {
56
59
  previewColumn: PreviewColumn;
@@ -90,6 +93,7 @@ export function ColumnExplorerPanel<TData>({
90
93
  totalColumns: effectiveTotalColumns,
91
94
  hiddenColumns: hiddenColumnCount,
92
95
  } = getColumnCountForDisplay(table, totalColumns);
96
+ const { visible: visibleColumnCount } = getUserColumnVisibilityCounts(table);
93
97
 
94
98
  const { rowsAndColumns, hiddenSuffix } = prettifyRowColumnCount({
95
99
  numRows: totalRows,
@@ -108,17 +112,34 @@ export function ColumnExplorerPanel<TData>({
108
112
  value={columns?.map(([columnName]) => columnName).join(",\n") || ""}
109
113
  className="h-3 w-3"
110
114
  />
111
- {hiddenColumnCount > 0 && (
112
- <Button
113
- type="button"
114
- variant="link"
115
- size="xs"
116
- className="h-auto p-0"
117
- onClick={() => table.resetColumnVisibility(true)}
118
- >
119
- Unhide all
120
- </Button>
121
- )}
115
+ <div className="ml-auto flex items-center gap-1">
116
+ <Tooltip content="Show all columns" delayDuration={400}>
117
+ <Button
118
+ type="button"
119
+ variant="ghost"
120
+ size="xs"
121
+ className="gap-1"
122
+ disabled={hiddenColumnCount === 0}
123
+ onClick={() => table.toggleAllColumnsVisible(true)}
124
+ >
125
+ <EyeIcon className="w-3 h-3" />
126
+ Show all
127
+ </Button>
128
+ </Tooltip>
129
+ <Tooltip content="Hide all columns" delayDuration={400}>
130
+ <Button
131
+ type="button"
132
+ variant="ghost"
133
+ size="xs"
134
+ className="gap-1"
135
+ disabled={visibleColumnCount === 0}
136
+ onClick={() => table.toggleAllColumnsVisible(false)}
137
+ >
138
+ <EyeOffIcon className="w-3 h-3" />
139
+ Hide all
140
+ </Button>
141
+ </Tooltip>
142
+ </div>
122
143
  </div>
123
144
  <Command className="h-5/6 bg-background" shouldFilter={false}>
124
145
  <CommandInput
@@ -59,6 +59,12 @@ export const ColumnVisibilityDropdown = <TData,>({
59
59
  const hiddenIds = userColumns
60
60
  .filter((column) => !column.getIsVisible())
61
61
  .map((column) => column.id);
62
+ const hideableIds = userColumns
63
+ .filter((column) => column.getCanHide())
64
+ .map((column) => column.id);
65
+ const visibleHideableCount = hideableIds.filter(
66
+ (id) => !hiddenIds.includes(id),
67
+ ).length;
62
68
 
63
69
  const applyHidden = (next: string[] | string | null) => {
64
70
  const hidden = new Set(Array.isArray(next) ? next : []);
@@ -127,6 +133,15 @@ export const ColumnVisibilityDropdown = <TData,>({
127
133
  <EyeIcon className="w-3 h-3 mr-1.5" />
128
134
  Show all
129
135
  </CommandItem>
136
+ <CommandItem
137
+ value="__hide_all__"
138
+ disabled={visibleHideableCount === 0}
139
+ onSelect={() => applyHidden(hideableIds)}
140
+ className="cursor-pointer"
141
+ >
142
+ <EyeOffIcon className="w-3 h-3 mr-1.5" />
143
+ Hide all
144
+ </CommandItem>
130
145
  <CommandSeparator />
131
146
  </>
132
147
  ) : (
@@ -31,7 +31,7 @@ import { cn } from "@/utils/cn";
31
31
  import { HideInKioskMode } from "../../kiosk-mode";
32
32
  import { ContributeSnippetButton } from "../components/contribute-snippet-button";
33
33
  import { usePanelOrientation, usePanelSection } from "./panel-context";
34
- import { getSnippetDisplay } from "./snippet-display";
34
+ import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
35
35
 
36
36
  const extensions = [EditorView.lineWrapping];
37
37
 
@@ -188,7 +188,7 @@ const SnippetViewer: React.FC<{ snippet: Snippet; onClose: () => void }> = ({
188
188
  return null;
189
189
  }
190
190
 
191
- const { language, value } = getSnippetDisplay(code);
191
+ const { language, code: displayCode } = getReadonlyCodeDisplay(code);
192
192
 
193
193
  return (
194
194
  <div
@@ -216,7 +216,7 @@ const SnippetViewer: React.FC<{ snippet: Snippet; onClose: () => void }> = ({
216
216
  language={language}
217
217
  className="cm border rounded overflow-hidden"
218
218
  extensions={extensions}
219
- value={value}
219
+ value={displayCode}
220
220
  readOnly={true}
221
221
  />
222
222
  </Suspense>
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { markdown } from "@codemirror/lang-markdown";
3
4
  import { sql } from "@codemirror/lang-sql";
4
5
  import CodeMirror, {
5
6
  EditorView,
@@ -11,6 +12,7 @@ import { useAddCodeToNewCell } from "@/components/editor/cell/useAddCell";
11
12
  import { Button } from "@/components/ui/button";
12
13
  import { Tooltip } from "@/components/ui/tooltip";
13
14
  import { toast } from "@/components/ui/use-toast";
15
+ import type { LanguageAdapterType } from "@/core/codemirror/language/types";
14
16
  import { customPythonLanguageSupport } from "@/core/codemirror/language/languages/python";
15
17
  import { useTheme } from "@/theme/useTheme";
16
18
  import { cn } from "@/utils/cn";
@@ -22,6 +24,18 @@ const pythonExtensions = [
22
24
  EditorView.lineWrapping,
23
25
  ];
24
26
  const sqlExtensions = [sql(), EditorView.lineWrapping];
27
+ const markdownExtensions = [markdown(), EditorView.lineWrapping];
28
+
29
+ function readonlyCodeExtensions(language: LanguageAdapterType) {
30
+ switch (language) {
31
+ case "sql":
32
+ return sqlExtensions;
33
+ case "markdown":
34
+ return markdownExtensions;
35
+ default:
36
+ return pythonExtensions;
37
+ }
38
+ }
25
39
 
26
40
  /**
27
41
  * A readonly code component that can be used to display code in a readonly state.
@@ -42,7 +56,7 @@ export const ReadonlyCode = memo(
42
56
  showHideCode?: boolean;
43
57
  showCopyCode?: boolean;
44
58
  insertNewCell?: boolean;
45
- language?: "python" | "sql";
59
+ language?: LanguageAdapterType;
46
60
  } & ReactCodeMirrorProps,
47
61
  ) => {
48
62
  const { theme } = useTheme();
@@ -84,7 +98,7 @@ export const ReadonlyCode = memo(
84
98
  theme={theme === "dark" ? "dark" : "light"}
85
99
  height="100%"
86
100
  editable={!hideCode}
87
- extensions={language === "python" ? pythonExtensions : sqlExtensions}
101
+ extensions={readonlyCodeExtensions(language)}
88
102
  value={code}
89
103
  readOnly={true}
90
104
  />
@@ -2,11 +2,7 @@
2
2
  /* oxlint-disable typescript/no-explicit-any */
3
3
  import { describe, expect, it } from "vitest";
4
4
  import type { OutputMessage } from "@/core/kernel/messages";
5
- import {
6
- groupCellsByColumn,
7
- readonlyDisplay,
8
- shouldHideCode,
9
- } from "../vertical-layout";
5
+ import { groupCellsByColumn, shouldHideCode } from "../vertical-layout";
10
6
 
11
7
  describe("groupCellsByColumn", () => {
12
8
  it("should group cells by column and maintain order", () => {
@@ -175,25 +171,3 @@ describe("shouldHideCode", () => {
175
171
  });
176
172
  });
177
173
  });
178
-
179
- describe("readonlyDisplay", () => {
180
- it("unwraps SQL cells to their inner query and marks them as sql", () => {
181
- const result = readonlyDisplay('my_table = mo.sql("""SELECT 1 AS id""")');
182
- expect(result.language).toBe("sql");
183
- expect(result.code).toBe("SELECT 1 AS id");
184
- });
185
-
186
- it("leaves plain Python cells untouched", () => {
187
- const code = "x = 1 + 2";
188
- const result = readonlyDisplay(code);
189
- expect(result.language).toBe("python");
190
- expect(result.code).toBe(code);
191
- });
192
-
193
- it("leaves non-SQL cells (e.g. markdown) unchanged as python", () => {
194
- const code = 'mo.md("""## Heading""")';
195
- const result = readonlyDisplay(code);
196
- expect(result.language).toBe("python");
197
- expect(result.code).toBe(code);
198
- });
199
- });
@@ -10,7 +10,6 @@ import {
10
10
  Loader2Icon,
11
11
  MoreHorizontalIcon,
12
12
  } from "lucide-react";
13
- import { SQLParser } from "@marimo-team/smart-cells";
14
13
  import type React from "react";
15
14
  import { memo, useRef, useState } from "react";
16
15
  import { z } from "zod";
@@ -30,6 +29,7 @@ import { outputIsLoading, outputIsStale } from "@/core/cells/cell";
30
29
  import type { CellId } from "@/core/cells/ids";
31
30
  import { isOutputEmpty } from "@/core/cells/outputs";
32
31
  import type { CellData, CellRuntimeState } from "@/core/cells/types";
32
+ import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
33
33
  import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown";
34
34
  import { useResolvedMarimoConfig } from "@/core/config/config";
35
35
  import { CSSClasses, KnownQueryParams } from "@/core/constants";
@@ -386,7 +386,7 @@ const VerticalCell = memo(
386
386
  // Hide the code if it's pure markdown and there's an output, or if the code is empty
387
387
  const hideCode = shouldHideCode(code, output);
388
388
  // Only unwrap SQL when the code will actually be rendered.
389
- const display = hideCode ? null : readonlyDisplay(code);
389
+ const display = hideCode ? null : getReadonlyCodeDisplay(code);
390
390
 
391
391
  return (
392
392
  <div
@@ -500,20 +500,3 @@ export function shouldHideCode(code: string, output: OutputMessage | null) {
500
500
  const hasOutput = output !== null && !isOutputEmpty(output);
501
501
  return (isPureMarkdown && hasOutput) || code.trim() === "";
502
502
  }
503
-
504
- const sqlParser = new SQLParser();
505
-
506
- /**
507
- * Unwrap SQL cells to their inner query so read-only views highlight them as
508
- * SQL instead of showing the raw `mo.sql(...)` wrapper.
509
- */
510
- export function readonlyDisplay(code: string): {
511
- code: string;
512
- language: "python" | "sql";
513
- } {
514
- const trimmed = code.trim();
515
- if (sqlParser.isSupported(trimmed)) {
516
- return { code: sqlParser.transformIn(trimmed).code, language: "sql" };
517
- }
518
- return { code, language: "python" };
519
- }
@@ -26,7 +26,7 @@ import { connectionAtom } from "@/core/network/connection";
26
26
  import { useTheme } from "@/theme/useTheme";
27
27
  import { cn } from "@/utils/cn";
28
28
  import { ReadonlyCode } from "../editor/code/readonly-python-code";
29
- import { languageAdapterFromCode } from "@/core/codemirror/language/extension";
29
+ import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
30
30
 
31
31
  type RuntimeCell = CellRuntimeState & CellData;
32
32
 
@@ -188,10 +188,7 @@ export const SlideCellReadOnlyView = ({ cell }: { cell: RuntimeCell }) => {
188
188
  const [userConfig] = useUserConfig();
189
189
  const cellOutputPosition = userConfig.display.cell_output;
190
190
 
191
- const language = useMemo(() => {
192
- const adapter = languageAdapterFromCode(cell.code.trim());
193
- return adapter.type === "sql" ? "sql" : "python";
194
- }, [cell.code]);
191
+ const display = useMemo(() => getReadonlyCodeDisplay(cell.code), [cell.code]);
195
192
 
196
193
  const output = (
197
194
  <CellOutputSlide
@@ -203,7 +200,11 @@ export const SlideCellReadOnlyView = ({ cell }: { cell: RuntimeCell }) => {
203
200
 
204
201
  const editor = (
205
202
  <div className="marimo-cell">
206
- <ReadonlyCode code={cell.code} language={language} showHideCode={false} />
203
+ <ReadonlyCode
204
+ code={display.code}
205
+ language={display.language}
206
+ showHideCode={false}
207
+ />
207
208
  </div>
208
209
  );
209
210
 
@@ -0,0 +1,46 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { getReadonlyCodeDisplay } from "../readonly-code-display";
5
+
6
+ describe("getReadonlyCodeDisplay", () => {
7
+ it("unwraps SQL cells to their inner query and marks them as sql", () => {
8
+ const result = getReadonlyCodeDisplay(
9
+ 'my_table = mo.sql("""SELECT 1 AS id""")',
10
+ );
11
+ expect(result.language).toBe("sql");
12
+ expect(result.code).toBe("SELECT 1 AS id");
13
+ });
14
+
15
+ it("unwraps f-string SQL cells", () => {
16
+ const result = getReadonlyCodeDisplay(
17
+ 'my_table = mo.sql(f"""SELECT 1 AS id""")',
18
+ );
19
+ expect(result.language).toBe("sql");
20
+ expect(result.code).toBe("SELECT 1 AS id");
21
+ });
22
+
23
+ it("leaves plain Python cells untouched", () => {
24
+ const code = "x = 1 + 2\nprint(x)";
25
+ const result = getReadonlyCodeDisplay(code);
26
+ expect(result.language).toBe("python");
27
+ expect(result.code).toBe(code);
28
+ });
29
+
30
+ it("unwraps markdown cells to their inner content", () => {
31
+ const result = getReadonlyCodeDisplay('mo.md("""## Heading""")');
32
+ expect(result.language).toBe("markdown");
33
+ expect(result.code).toBe("## Heading");
34
+ });
35
+
36
+ it("treats empty or whitespace-only code as python", () => {
37
+ expect(getReadonlyCodeDisplay("")).toEqual({
38
+ code: "",
39
+ language: "python",
40
+ });
41
+ expect(getReadonlyCodeDisplay(" \n\t ")).toEqual({
42
+ code: " \n\t ",
43
+ language: "python",
44
+ });
45
+ });
46
+ });
@@ -0,0 +1,35 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { MarkdownParser, SQLParser } from "@marimo-team/smart-cells";
4
+ import type { LanguageAdapterType } from "@/core/codemirror/language/types";
5
+
6
+ export interface ReadonlyCodeDisplay {
7
+ code: string;
8
+ language: LanguageAdapterType;
9
+ }
10
+
11
+ const markdownParser = new MarkdownParser();
12
+ const sqlParser = new SQLParser();
13
+
14
+ /**
15
+ * Unwrap SQL and markdown cells so read-only views show inner content with the
16
+ * correct syntax highlighting instead of the raw Python wrapper.
17
+ */
18
+ export function getReadonlyCodeDisplay(code: string): ReadonlyCodeDisplay {
19
+ const trimmed = code.trim();
20
+ if (!trimmed) {
21
+ return { code, language: "python" };
22
+ }
23
+
24
+ if (markdownParser.isSupported(trimmed)) {
25
+ return {
26
+ code: markdownParser.transformIn(trimmed).code,
27
+ language: "markdown",
28
+ };
29
+ }
30
+
31
+ if (sqlParser.isSupported(trimmed)) {
32
+ return { code: sqlParser.transformIn(trimmed).code, language: "sql" };
33
+ }
34
+ return { code, language: "python" };
35
+ }
@@ -1,22 +0,0 @@
1
- /* Copyright 2026 Marimo. All rights reserved. */
2
-
3
- import { describe, expect, it } from "vitest";
4
- import { getSnippetDisplay } from "../snippet-display";
5
-
6
- describe("getSnippetDisplay", () => {
7
- it("shows sql cells as the sql query", () => {
8
- const { language, value } = getSnippetDisplay(
9
- 'df = mo.sql("""SELECT * FROM users LIMIT 5""")',
10
- );
11
- expect(language).toBe("sql");
12
- expect(value).toBe("SELECT * FROM users LIMIT 5");
13
- });
14
-
15
- it("keeps plain python cells as python", () => {
16
- const code = "x = 1 + 2\nprint(x)";
17
- expect(getSnippetDisplay(code)).toEqual({
18
- language: "python",
19
- value: code,
20
- });
21
- });
22
- });