@marimo-team/frontend 0.23.13-dev0 → 0.23.13-dev2

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.
Files changed (73) hide show
  1. package/dist/assets/{CellStatus-xkT4Dvj4.js → CellStatus-B43aF1fe.js} +1 -1
  2. package/dist/assets/{JsonOutput-FFLZqPUK.js → JsonOutput-DTeVFctC.js} +4 -4
  3. package/dist/assets/{MarimoErrorOutput-BQt_FXJL.js → MarimoErrorOutput-DcImOp4r.js} +2 -2
  4. package/dist/assets/{RenderHTML-Cq4mwWYz.js → RenderHTML-BLBwT24G.js} +1 -1
  5. package/dist/assets/{RunButton-DdQ4s_KP.js → RunButton-CYnmbljZ.js} +1 -1
  6. package/dist/assets/{add-cell-with-ai-wdY7HT-D.js → add-cell-with-ai-8yk94rYa.js} +8 -8
  7. package/dist/assets/{add-connection-dialog-C41ThK2T.js → add-connection-dialog-CXW90r_2.js} +1 -1
  8. package/dist/assets/{agent-panel-q5ey8yNH.js → agent-panel-DjJhOAu6.js} +2 -2
  9. package/dist/assets/{ai-model-dropdown-DQb_CyMN.js → ai-model-dropdown-CQOgbiDG.js} +3 -3
  10. package/dist/assets/{app-config-button-8FXNdkBs.js → app-config-button-BzGthL1g.js} +1 -1
  11. package/dist/assets/cell-editor-IrwQ6wV6.js +27 -0
  12. package/dist/assets/{cell-link-DgWTiCtG.js → cell-link-LArpBcY-.js} +1 -1
  13. package/dist/assets/{cells-06TDmJ8V.js → cells-DQ5n6Zef.js} +37 -37
  14. package/dist/assets/{chat-display-DO7nc_uY.js → chat-display-CJa1DlwN.js} +1 -1
  15. package/dist/assets/{chat-panel-DmUkj42a.js → chat-panel-BY4ctHmZ.js} +1 -1
  16. package/dist/assets/{chat-ui-Bf7t-NAW.js → chat-ui-BieByIci.js} +1 -1
  17. package/dist/assets/{column-preview-oHsVFerC.js → column-preview-VcOeHtkt.js} +1 -1
  18. package/dist/assets/{command-palette-Bvl7fQ8h.js → command-palette-DtV4W1aF.js} +1 -1
  19. package/dist/assets/{common-cKvfH0fD.js → common-XGVukwY0.js} +1 -1
  20. package/dist/assets/{components-BhmX9dcm.js → components-C2HqGrL_.js} +1 -1
  21. package/dist/assets/{datasource-mVfTJz3V.js → datasource-BT-L2LDD.js} +2 -2
  22. package/dist/assets/{dependency-graph-panel-D9C_hBQw.js → dependency-graph-panel-DGDP7vz5.js} +1 -1
  23. package/dist/assets/{documentation-panel-C0uXzYri.js → documentation-panel-ODnRqsmJ.js} +1 -1
  24. package/dist/assets/{download-VxqV5OSK.js → download-rKGlekSQ.js} +4 -4
  25. package/dist/assets/{edit-page-CInfJ1ps.js → edit-page-DeFAhK8V.js} +6 -6
  26. package/dist/assets/{error-panel-D2WmJDnT.js → error-panel-BjCDBUEW.js} +1 -1
  27. package/dist/assets/{file-explorer-panel-nn5I1oLj.js → file-explorer-panel-UL0ND-sz.js} +3 -3
  28. package/dist/assets/{file-icons-BeJWUbBH.js → file-icons-DBMviz_K.js} +1 -1
  29. package/dist/assets/{file-name-input-Slq_KVFS.js → file-name-input-C1PsBoII.js} +1 -1
  30. package/dist/assets/{floating-outline-3UBj62S1.js → floating-outline-CPTvLHMl.js} +1 -1
  31. package/dist/assets/{focus-DqhVePQT.js → focus-Dol5ybck.js} +1 -1
  32. package/dist/assets/{form-pEJO4Mq_.js → form-B9I9mpZy.js} +1 -1
  33. package/dist/assets/{home-page-CFgoMQu4.js → home-page-D05XGteH.js} +2 -2
  34. package/dist/assets/{hooks-NssnE3R8.js → hooks-CO3SDGZ2.js} +1 -1
  35. package/dist/assets/{html-to-image-CbQeOjEJ.js → html-to-image-CmK-WSPG.js} +1 -1
  36. package/dist/assets/{index-BjsskD4V.js → index-iAj2M3os.js} +9 -9
  37. package/dist/assets/{kiosk-mode-CyYnV3_y.js → kiosk-mode-CAa5-suK.js} +1 -1
  38. package/dist/assets/{layout-ChW1-odP.js → layout-BANwNeHe.js} +4 -4
  39. package/dist/assets/{logs-panel-CiP_30S4.js → logs-panel-DJyCkydj.js} +1 -1
  40. package/dist/assets/{markdown-renderer-C1yypOYF.js → markdown-renderer-f3mfGw8u.js} +1 -1
  41. package/dist/assets/{name-cell-input-CIkFEB6-.js → name-cell-input-DteS1CD8.js} +1 -1
  42. package/dist/assets/{outline-panel-BfysapqU.js → outline-panel-FvxWzBOw.js} +1 -1
  43. package/dist/assets/{packages-panel-BYm2thcs.js → packages-panel-wjIUaBYs.js} +1 -1
  44. package/dist/assets/{panels-hLpsLawB.js → panels-BQOW0GWX.js} +1 -1
  45. package/dist/assets/{process-output-BIlXY0iE.js → process-output-Ll90qx0s.js} +1 -1
  46. package/dist/assets/{radio-group-BaOG2bSA.js → radio-group-D7RG9Px_.js} +1 -1
  47. package/dist/assets/{readonly-python-code-CNyK8G1T.js → readonly-python-code-DHJTIc4H.js} +1 -1
  48. package/dist/assets/{reveal-component-D0boNLsp.js → reveal-component-CfuKjozU.js} +1 -1
  49. package/dist/assets/{run-page-B0OR2l0l.js → run-page-DFWelrro.js} +1 -1
  50. package/dist/assets/{scratchpad-panel-Di7uCP6T.js → scratchpad-panel-BN-eTAIv.js} +1 -1
  51. package/dist/assets/{session-panel-D6GA7GoC.js → session-panel-Dywriyca.js} +1 -1
  52. package/dist/assets/{snippets-panel-DV2ir7q8.js → snippets-panel-Ee_wUGYF.js} +1 -1
  53. package/dist/assets/{state-DD6m9X2h.js → state-D7fu7IC3.js} +3 -3
  54. package/dist/assets/state-VxOqftwq.js +1 -0
  55. package/dist/assets/{textarea-BM7hAwR3.js → textarea-DD5BwHTh.js} +1 -1
  56. package/dist/assets/{tracing-DwjgsPkU.js → tracing-Ckq9673r.js} +1 -1
  57. package/dist/assets/{tracing-panel-BS9eUNK_.js → tracing-panel-7UPP4OM8.js} +2 -2
  58. package/dist/assets/{tree-actions-lVyJAYy7.js → tree-actions-CjikjOBA.js} +1 -1
  59. package/dist/assets/{useCellActionButton-B88iHApv.js → useCellActionButton-TRa0-mam.js} +1 -1
  60. package/dist/assets/{useDeleteCell-CXc93le9.js → useDeleteCell-CICC5IPM.js} +1 -1
  61. package/dist/assets/{useDependencyPanelTab-9Vnr8-OR.js → useDependencyPanelTab-DUmpfdwT.js} +1 -1
  62. package/dist/assets/{useNotebookActions-DSfevQ7S.js → useNotebookActions-Z9M92mue.js} +1 -1
  63. package/dist/assets/useRunCells-bcK6jTD2.js +1 -0
  64. package/dist/assets/{useSplitCell-IsXuVcLF.js → useSplitCell-CqcM6MQ1.js} +1 -1
  65. package/dist/index.html +22 -22
  66. package/package.json +1 -1
  67. package/src/components/editor/cell/code/cell-editor.tsx +33 -1
  68. package/src/core/codemirror/cells/state.ts +5 -0
  69. package/src/core/codemirror/misc/__tests__/paste.test.ts +190 -1
  70. package/src/core/codemirror/misc/paste.ts +130 -114
  71. package/dist/assets/cell-editor-CFq9iL3S.js +0 -24
  72. package/dist/assets/state-B3ypjIx-.js +0 -1
  73. package/dist/assets/useRunCells-DNaOKKqW.js +0 -1
@@ -1,6 +1,8 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import type { Extension } from "@codemirror/state";
3
3
  import { EditorView } from "@codemirror/view";
4
+ import type { SyntaxNode } from "@lezer/common";
5
+ import { parser } from "@lezer/python";
4
6
  import { cellActionsState } from "../cells/state";
5
7
 
6
8
  export function pasteBundle(): Extension[] {
@@ -8,152 +10,166 @@ export function pasteBundle(): Extension[] {
8
10
  EditorView.domEventHandlers({
9
11
  paste: (event: ClipboardEvent, view: EditorView) => {
10
12
  const text = event.clipboardData?.getData("text/plain");
11
- if (!text?.includes("@app.cell")) {
13
+ if (!text || !looksLikeMarimoApp(text)) {
12
14
  return false;
13
15
  }
14
16
 
15
- const cells = extractCells(text);
16
- if (cells.length === 0) {
17
+ const { setup, cells } = extractMarimoApp(text);
18
+ if (setup === null && cells.length === 0) {
17
19
  return false;
18
20
  }
19
21
 
20
22
  const actions = view.state.facet(cellActionsState);
21
- actions.createManyBelow(cells);
23
+ // The `with app.setup` block belongs in the special setup cell, not in
24
+ // an ordinary cell.
25
+ if (setup !== null) {
26
+ actions.addOrAppendSetupCell(setup);
27
+ }
28
+ if (cells.length > 0) {
29
+ actions.createManyBelow(cells);
30
+ }
22
31
  return true;
23
32
  },
24
33
  }),
25
34
  ];
26
35
  }
27
36
 
37
+ /** Whether the pasted text looks like marimo app source worth parsing. */
38
+ function looksLikeMarimoApp(text: string): boolean {
39
+ return /@app\.cell|@app\.function|\bapp\.setup\b/.test(text);
40
+ }
41
+
42
+ export interface ExtractedApp {
43
+ /** The `with app.setup` block's body, if present. Belongs in the setup cell. */
44
+ setup: string | null;
45
+ /** Ordinary cells, in source order. */
46
+ cells: string[];
47
+ }
48
+
28
49
  /**
29
- * Extract the cells from a marimo app.
50
+ * Extract the cells of a marimo app from its source.
51
+ *
52
+ * Parses the pasted source with the Python grammar (the same one that powers
53
+ * the editor) and turns each marimo construct into a cell:
54
+ * - `with app.setup(...)` contributes the setup block's body, returned
55
+ * separately as `setup` because it belongs in the dedicated setup cell.
56
+ * - `@app.cell` functions contribute their body, minus the trailing
57
+ * auto-generated `return`.
58
+ * - `@app.function` definitions contribute the whole function (the decorator
59
+ * is dropped, but the function — including its `return` — is kept).
60
+ *
61
+ * Using the real syntax tree means decorators, `async def`, multi-line
62
+ * signatures, nested functions, and multi-line returns of any bracket type are
63
+ * all handled structurally rather than by line-based heuristics.
30
64
  */
31
- export function extractCells(text: string): string[] {
32
- // Quick check if this looks like a marimo app
33
- if (!text.includes("@app.cell")) {
34
- return [];
65
+ export function extractMarimoApp(text: string): ExtractedApp {
66
+ if (!looksLikeMarimoApp(text)) {
67
+ return { setup: null, cells: [] };
35
68
  }
36
69
 
37
70
  const cells: string[] = [];
38
- const lines = text.split("\n");
39
- let currentCell: string[] = [];
40
- let inCell = false;
41
- let skipLines = 0;
42
- let inMultilineArgs = false;
43
- let inMultilineReturn = false;
44
- let parenCount = 0;
45
- let cellBaseIndent: number | null = null;
46
-
47
- // Pre-compile regex patterns
48
- const leadingParenRegex = /\(/g;
49
- const trailingParenRegex = /\)/g;
50
- const cellEndMarkers = new Set(["@"]);
51
-
52
- function countParens(line: string): number {
53
- return (
54
- (line.match(leadingParenRegex) || []).length -
55
- (line.match(trailingParenRegex) || []).length
56
- );
57
- }
58
-
59
- function getIndent(line: string): number {
60
- const match = line.match(/^\s*/);
61
- return match ? match[0].length : 0;
62
- }
63
-
64
- function finalizeCellIfNeeded() {
65
- if (currentCell.length === 0) {
66
- return;
71
+ const setupBlocks: string[] = [];
72
+ const tree = parser.parse(text);
73
+
74
+ // A node's `from` starts after its line's leading indentation; extend back to
75
+ // the start of the line so `dedent` sees consistent indentation, then dedent.
76
+ const blockText = (from: number, to: number): string => {
77
+ const lineStart = text.lastIndexOf("\n", from - 1) + 1;
78
+ return dedent(text.slice(lineStart, to));
79
+ };
80
+
81
+ const pushCell = (from: number, to: number) => {
82
+ const cell = blockText(from, to);
83
+ if (cell.trim()) {
84
+ cells.push(cell);
67
85
  }
68
-
69
- // Only add non-empty cells
70
- if (currentCell.some((l) => l.trim() !== "")) {
71
- cells.push(dedent(currentCell.join("\n")));
86
+ };
87
+
88
+ // The text of a block's body (the statements after its `:`), optionally
89
+ // dropping a trailing auto-generated `return`. Returns null if empty.
90
+ const bodyText = (
91
+ body: SyntaxNode,
92
+ stripTrailingReturn: boolean,
93
+ ): string | null => {
94
+ const children: SyntaxNode[] = [];
95
+ for (let child = body.firstChild; child; child = child.nextSibling) {
96
+ if (child.name !== ":") {
97
+ children.push(child);
98
+ }
72
99
  }
73
- currentCell = [];
74
- }
75
-
76
- for (const line of lines) {
77
- const trimmed = line.trim();
78
-
79
- // Skip empty lines between cells
80
- if (!trimmed && !inCell) {
81
- continue;
100
+ if (stripTrailingReturn && children.at(-1)?.name === "ReturnStatement") {
101
+ children.pop();
82
102
  }
83
-
84
- // Start of a new cell
85
- if (trimmed.startsWith("@app.cell")) {
86
- finalizeCellIfNeeded();
87
- inCell = true;
88
- skipLines = 1; // Skip the def line
89
- cellBaseIndent = null;
90
- continue;
103
+ const first = children[0];
104
+ const last = children.at(-1);
105
+ if (!first || !last) {
106
+ return null;
91
107
  }
92
-
93
- // Handle function definition and args
94
- if (skipLines > 0) {
95
- if (
96
- trimmed.startsWith("def") &&
97
- trimmed.includes("(") &&
98
- !trimmed.includes("):")
99
- ) {
100
- inMultilineArgs = true;
101
- parenCount = countParens(trimmed);
108
+ const cell = blockText(first.from, last.to);
109
+ return cell.trim() ? cell : null;
110
+ };
111
+
112
+ tree.iterate({
113
+ enter: (node) => {
114
+ // Setup cell: `with app.setup:` / `with app.setup(...):`
115
+ if (node.name === "WithStatement") {
116
+ const body = node.node.getChild("Body");
117
+ if (body && text.slice(node.from, body.from).includes("app.setup")) {
118
+ const setup = bodyText(body, false);
119
+ if (setup !== null) {
120
+ setupBlocks.push(setup);
121
+ }
122
+ }
123
+ return false;
102
124
  }
103
- skipLines--;
104
- continue;
105
- }
106
125
 
107
- // Track multi-line arguments
108
- if (inMultilineArgs) {
109
- parenCount += countParens(trimmed);
110
- if (parenCount === 0) {
111
- inMultilineArgs = false;
126
+ if (node.name !== "DecoratedStatement") {
127
+ // Keep descending until we reach the decorated definitions.
128
+ return true;
112
129
  }
113
- continue;
114
- }
115
-
116
- if (!inCell) {
117
- continue;
118
- }
119
-
120
- // Handle cell content
121
- if (cellEndMarkers.has(trimmed[0]) || trimmed.startsWith("if __name__")) {
122
- finalizeCellIfNeeded();
123
- inCell = trimmed.startsWith("@");
124
- continue;
125
- }
126
-
127
- // Detect base indentation of cell body from first content line
128
- if (cellBaseIndent === null && trimmed) {
129
- cellBaseIndent = getIndent(line);
130
- }
131
130
 
132
- // Handle return statements — only strip cell-level returns
133
- if (trimmed.startsWith("return") && getIndent(line) === cellBaseIndent) {
134
- if (trimmed.includes("(") && !trimmed.endsWith(")")) {
135
- inMultilineReturn = true;
136
- parenCount = countParens(trimmed);
131
+ const decorator = node.node.getChild("Decorator");
132
+ const fn = node.node.getChild("FunctionDefinition");
133
+ if (!decorator || !fn) {
134
+ return false;
135
+ }
136
+ const decoratorText = text.slice(decorator.from, decorator.to).trim();
137
+
138
+ // `@app.cell` / `@app.cell(...)`: the body becomes a cell, minus the
139
+ // trailing auto-generated return.
140
+ if (decoratorText.startsWith("@app.cell")) {
141
+ const body = fn.getChild("Body");
142
+ if (body) {
143
+ const cell = bodyText(body, true);
144
+ if (cell !== null) {
145
+ cells.push(cell);
146
+ }
147
+ }
148
+ return false;
137
149
  }
138
- continue;
139
- }
140
150
 
141
- if (inMultilineReturn) {
142
- parenCount += countParens(trimmed);
143
- if (parenCount === 0) {
144
- inMultilineReturn = false;
151
+ // `@app.function`: the whole function definition is the cell (the
152
+ // decorator is dropped, but the function — including its return — stays).
153
+ if (decoratorText.startsWith("@app.function")) {
154
+ pushCell(fn.from, fn.to);
145
155
  }
146
- continue;
147
- }
148
156
 
149
- // Add line to current cell
150
- currentCell.push(line);
151
- }
157
+ return false;
158
+ },
159
+ });
152
160
 
153
- // Handle last cell
154
- finalizeCellIfNeeded();
161
+ return {
162
+ setup: setupBlocks.length > 0 ? setupBlocks.join("\n\n") : null,
163
+ cells,
164
+ };
165
+ }
155
166
 
156
- return cells;
167
+ /**
168
+ * Convenience wrapper returning only the ordinary cells (excluding the setup
169
+ * block). Prefer {@link extractMarimoApp} when the setup block matters.
170
+ */
171
+ export function extractCells(text: string): string[] {
172
+ return extractMarimoApp(text).cells;
157
173
  }
158
174
 
159
175
  function dedent(text: string): string {