@marimo-team/islands 0.23.16-dev6 → 0.23.16-dev62

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 (198) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CJaUI-S-.js → ConnectedDataExplorerComponent-f9vLjQiF.js} +13 -13
  2. package/dist/{ErrorBoundary-DXlxsrTF.js → ErrorBoundary-DhLqwSWA.js} +3 -3
  3. package/dist/{any-language-editor-P9tjkj4n.js → any-language-editor-DifE-fl7.js} +5 -5
  4. package/dist/{button-BcSml68y.js → button-BSm8IXRU.js} +2 -4
  5. package/dist/{capabilities-BYLFYTYv.js → capabilities-C6OQ5YBO.js} +1 -1
  6. package/dist/{chat-ui-BdSDiVjk.js → chat-ui-DsZJj75A.js} +3117 -3120
  7. package/dist/{check-Cs-KV5XY.js → check-DP7U0H0G.js} +1 -1
  8. package/dist/{code-visibility-CfBftiVM.js → common-Bpnmu1Yz.js} +1085 -1053
  9. package/dist/{copy-Cs3yt6Vt.js → copy-DNq8q3zw.js} +2 -2
  10. package/dist/{dist-B96tMAst.js → dist-DIlxLFXG.js} +2 -2
  11. package/dist/{error-banner-DR7o3CbX.js → error-banner-B_GQfxxD.js} +2 -2
  12. package/dist/{esm-k8PIvuZW.js → esm-DzZVaNH4.js} +1 -1
  13. package/dist/{extends-Cq2JYHKy.js → extends-w2_S44bV.js} +4 -4
  14. package/dist/{formats-BFhkZJwd.js → formats-Dwq1THhr.js} +3 -3
  15. package/dist/{glide-data-editor--CaN-f27.js → glide-data-editor-B9K6-MJe.js} +9 -9
  16. package/dist/{html-to-image-DDENewl2.js → html-to-image-CZ1kLKkq.js} +7350 -6356
  17. package/dist/{input-DStQIuqD.js → input-C5K4VBvB.js} +7 -7
  18. package/dist/{label-DIk0QoFe.js → label-DjoYwqmj.js} +2 -2
  19. package/dist/{loader-CTHCI-qe.js → loader-DX-IDS7n.js} +1 -1
  20. package/dist/main.js +259 -194
  21. package/dist/{mermaid-CdSvixW2.js → mermaid-hv6IckVw.js} +5 -5
  22. package/dist/{node-sql-parser-DLl_zNw0.js → node-sql-parser-v1Vi-Fft.js} +16867 -16665
  23. package/dist/{process-output-2QPcgBQg.js → process-output-bVfbcx5_.js} +32 -28
  24. package/dist/{reveal-component-BclLkYnV.js → reveal-component-JIt3oSeU.js} +642 -634
  25. package/dist/{spec-CnaxwDqy.js → spec-CQKL_D3i.js} +4 -4
  26. package/dist/{strings-BYAhZnQi.js → strings-DIy9NAmg.js} +4 -4
  27. package/dist/style.css +1 -1
  28. package/dist/{swiper-component-DE94711u.js → swiper-component-CEdAYjb7.js} +2 -2
  29. package/dist/toDate-CqhBWjvO.js +713 -0
  30. package/dist/{tooltip-B-qh7UPB.js → tooltip-f-qR0iUx.js} +3 -3
  31. package/dist/{types-DQl7jVYK.js → types-DicH9Oi0.js} +1 -1
  32. package/dist/{useAsyncData-CvO8WUy3.js → useAsyncData-CYOQb1EW.js} +1 -1
  33. package/dist/{useDateFormatter-BSoGim-G.js → useDateFormatter-Bt2qr_XS.js} +2 -2
  34. package/dist/{useDeepCompareMemoize-J3MKRdA4.js → useDeepCompareMemoize-CSHQeQEI.js} +1 -1
  35. package/dist/{useIframeCapabilities-POcFXHbw.js → useIframeCapabilities-kA09ykNq.js} +1 -1
  36. package/dist/{useLifecycle-DqPxHgAN.js → useLifecycle-CMJGqBaY.js} +3 -3
  37. package/dist/{useTheme-XMq9mNf_.js → useTheme-DYfby65G.js} +2 -2
  38. package/dist/{vega-component-CrFoZT1S.js → vega-component-C7nO5zcf.js} +10 -10
  39. package/dist/{zod-D0yOqoGe.js → zod-BUcyXSdR.js} +1 -1
  40. package/package.json +3 -4
  41. package/src/__mocks__/requests.ts +56 -4
  42. package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
  43. package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
  44. package/src/components/data-table/__tests__/columns.test.tsx +9 -1
  45. package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
  46. package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
  47. package/src/components/data-table/column-summary/utils.ts +26 -7
  48. package/src/components/data-table/columns.tsx +11 -1
  49. package/src/components/data-table/types.ts +1 -1
  50. package/src/components/editor/__tests__/viewer-banner.test.tsx +14 -0
  51. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +99 -18
  52. package/src/components/editor/actions/__tests__/pdf-export.test.ts +32 -13
  53. package/src/components/editor/actions/pair-with-agent-commands.ts +21 -10
  54. package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
  55. package/src/components/editor/actions/pdf-export.ts +11 -14
  56. package/src/components/editor/actions/useNotebookActions.tsx +28 -19
  57. package/src/components/editor/cell/code/__tests__/cell-editor-activity-lifecycle.test.ts +250 -0
  58. package/src/components/editor/cell/code/cell-editor.tsx +13 -7
  59. package/src/components/editor/chrome/__tests__/state.test.ts +7 -1
  60. package/src/components/editor/chrome/panels/__tests__/logs-panel.test.tsx +25 -0
  61. package/src/components/editor/chrome/panels/__tests__/panel-accordion-state.test.ts +42 -0
  62. package/src/components/editor/chrome/panels/file-explorer-panel.tsx +6 -17
  63. package/src/components/editor/chrome/panels/logs-panel.tsx +12 -2
  64. package/src/components/editor/chrome/panels/panel-accordion-state.ts +51 -0
  65. package/src/components/editor/chrome/panels/session-panel.tsx +5 -16
  66. package/src/components/editor/chrome/panels/write-secret-modal.tsx +40 -12
  67. package/src/components/editor/chrome/state.ts +10 -0
  68. package/src/components/editor/connections/__tests__/json-credentials.test.ts +114 -0
  69. package/src/components/editor/connections/__tests__/paths.test.ts +39 -0
  70. package/src/components/editor/connections/__tests__/quick-add-data-sources.test.tsx +113 -0
  71. package/src/components/editor/connections/add-connection-dialog.tsx +2 -0
  72. package/src/components/editor/connections/components.tsx +6 -2
  73. package/src/components/editor/connections/database/__tests__/__snapshots__/as-code.test.ts.snap +27 -5
  74. package/src/components/editor/connections/database/__tests__/as-code.test.ts +33 -0
  75. package/src/components/editor/connections/database/as-code.ts +37 -13
  76. package/src/components/editor/connections/database/schemas.ts +6 -5
  77. package/src/components/editor/connections/form-renderers.tsx +99 -1
  78. package/src/components/editor/connections/json-credentials.ts +57 -0
  79. package/src/components/editor/connections/paths.ts +21 -0
  80. package/src/components/editor/connections/quick-add-data-sources.tsx +106 -0
  81. package/src/components/editor/connections/secret-combobox.tsx +41 -20
  82. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +88 -2
  83. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +128 -8
  84. package/src/components/editor/connections/storage/as-code.ts +79 -36
  85. package/src/components/editor/connections/storage/schemas.ts +85 -44
  86. package/src/components/editor/file-tree/__tests__/renderers.test.ts +31 -0
  87. package/src/components/editor/file-tree/file-viewer.tsx +11 -7
  88. package/src/components/editor/file-tree/renderers.tsx +21 -0
  89. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +2 -2
  90. package/src/components/editor/viewer-banner.tsx +6 -1
  91. package/src/components/forms/form.tsx +59 -24
  92. package/src/components/forms/options.ts +2 -1
  93. package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
  94. package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
  95. package/src/components/slides/reveal-component.tsx +22 -17
  96. package/src/components/slides/slide-cell-view.tsx +1 -1
  97. package/src/components/slides/slides.css +4 -2
  98. package/src/components/static-html/share-modal.tsx +1 -1
  99. package/src/components/ui/input.tsx +1 -1
  100. package/src/core/ai/context/providers/file.ts +8 -29
  101. package/src/core/cells/__tests__/cells.test.ts +12 -2
  102. package/src/core/cells/__tests__/logs.test.ts +5 -5
  103. package/src/core/cells/cells.ts +17 -4
  104. package/src/core/cells/document-changes.ts +1 -0
  105. package/src/core/cells/logs.ts +14 -4
  106. package/src/core/codemirror/cm.ts +3 -0
  107. package/src/core/codemirror/code-lens/__tests__/actions.test.ts +68 -0
  108. package/src/core/codemirror/code-lens/__tests__/analyzer.test.ts +224 -0
  109. package/src/core/codemirror/code-lens/__tests__/entities.test.ts +175 -0
  110. package/src/core/codemirror/code-lens/__tests__/extension.test.ts +325 -0
  111. package/src/core/codemirror/code-lens/__tests__/perf.test.ts +121 -0
  112. package/src/core/codemirror/code-lens/__tests__/popover.test.ts +93 -0
  113. package/src/core/codemirror/code-lens/actions.ts +33 -0
  114. package/src/core/codemirror/code-lens/analyzer.ts +284 -0
  115. package/src/core/codemirror/code-lens/entities.ts +123 -0
  116. package/src/core/codemirror/code-lens/extension.ts +298 -0
  117. package/src/core/codemirror/code-lens/icons.ts +36 -0
  118. package/src/core/codemirror/code-lens/popover.tsx +240 -0
  119. package/src/core/codemirror/copilot/__tests__/language-server.test.ts +396 -100
  120. package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -540
  121. package/src/core/codemirror/copilot/language-server.ts +182 -70
  122. package/src/core/codemirror/copilot/transport.ts +15 -207
  123. package/src/core/codemirror/copilot/types.ts +9 -2
  124. package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
  125. package/src/core/codemirror/find-replace/navigate.ts +2 -0
  126. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
  127. package/src/core/codemirror/go-to-definition/commands.ts +93 -90
  128. package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
  129. package/src/core/codemirror/keymaps/keymaps.ts +12 -0
  130. package/src/core/codemirror/language/__tests__/sql.test.ts +627 -61
  131. package/src/core/codemirror/language/languages/python.ts +23 -11
  132. package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
  133. package/src/core/codemirror/language/languages/sql/renderers.tsx +3 -3
  134. package/src/core/codemirror/language/languages/sql/sql.ts +251 -16
  135. package/src/core/codemirror/lsp/__tests__/federated-lsp.test.ts +161 -0
  136. package/src/core/codemirror/lsp/__tests__/log-messages.test.ts +53 -0
  137. package/src/core/codemirror/lsp/__tests__/normalize-markdown-math.test.ts +25 -0
  138. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +598 -156
  139. package/src/core/codemirror/lsp/__tests__/transports.test.ts +112 -0
  140. package/src/core/codemirror/lsp/federated-lsp.ts +76 -23
  141. package/src/core/codemirror/lsp/log-messages.ts +37 -0
  142. package/src/core/codemirror/lsp/normalize-markdown-math.ts +8 -4
  143. package/src/core/codemirror/lsp/notebook-lsp.ts +220 -56
  144. package/src/core/codemirror/lsp/transports.ts +8 -1
  145. package/src/core/codemirror/lsp/types.ts +68 -8
  146. package/src/core/codemirror/lsp/utils.ts +14 -0
  147. package/src/core/codemirror/python-node-names.ts +90 -0
  148. package/src/core/codemirror/reactive-references/analyzer.ts +52 -57
  149. package/src/core/codemirror/utils.ts +28 -0
  150. package/src/core/config/feature-flag.tsx +3 -1
  151. package/src/core/datasets/data-source-discovery.ts +5 -0
  152. package/src/core/datasets/request-registry.ts +11 -0
  153. package/src/core/edit-app.tsx +1 -2
  154. package/src/core/export/__tests__/hooks.test.ts +2 -4
  155. package/src/core/export/hooks.ts +4 -3
  156. package/src/core/hotkeys/hotkeys.ts +0 -2
  157. package/src/core/islands/__tests__/bridge.test.ts +107 -0
  158. package/src/core/islands/bootstrap.ts +1 -0
  159. package/src/core/islands/bridge.ts +18 -0
  160. package/src/core/kernel/messages.ts +2 -0
  161. package/src/core/lsp/__tests__/transport.test.ts +509 -337
  162. package/src/core/lsp/transport.ts +304 -132
  163. package/src/core/mode.ts +10 -0
  164. package/src/core/network/__tests__/api.test.ts +42 -0
  165. package/src/core/network/__tests__/connection.test.ts +45 -0
  166. package/src/core/network/__tests__/requests-lazy.test.ts +47 -1
  167. package/src/core/network/__tests__/requests-network.test.ts +48 -0
  168. package/src/core/network/api.ts +27 -0
  169. package/src/core/network/connection.ts +15 -0
  170. package/src/core/network/requests-lazy.ts +3 -0
  171. package/src/core/network/requests-network.ts +31 -8
  172. package/src/core/network/requests-static.ts +3 -0
  173. package/src/core/network/requests-toasting.tsx +3 -0
  174. package/src/core/network/types.ts +27 -6
  175. package/src/core/rtc/state.ts +9 -0
  176. package/src/core/static/download-html.ts +5 -14
  177. package/src/core/wasm/__tests__/bridge.test.ts +99 -5
  178. package/src/core/wasm/bridge.ts +42 -9
  179. package/src/core/wasm/worker/types.ts +10 -3
  180. package/src/core/wasm/worker/worker.ts +7 -8
  181. package/src/core/websocket/useMarimoKernelConnection.tsx +4 -0
  182. package/src/css/app/codemirror.css +16 -2
  183. package/src/hooks/__tests__/useDataSourceDiscovery.test.ts +80 -0
  184. package/src/hooks/useDataSourceDiscovery.ts +18 -0
  185. package/src/plugins/impl/DataTablePlugin.tsx +4 -1
  186. package/src/plugins/impl/FormPlugin.tsx +1 -1
  187. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +158 -0
  188. package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
  189. package/src/plugins/impl/anywidget/__tests__/model.test.ts +55 -0
  190. package/src/plugins/impl/anywidget/model.ts +20 -5
  191. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +33 -21
  192. package/src/plugins/impl/data-frames/forms/renderers.tsx +11 -8
  193. package/src/stories/log-viewer.stories.tsx +12 -6
  194. package/src/utils/__tests__/blob.test.ts +27 -1
  195. package/src/utils/__tests__/download.test.tsx +44 -4
  196. package/src/utils/blob.ts +24 -1
  197. package/src/utils/download.ts +14 -15
  198. package/dist/toDate-B3IRfHhw.js +0 -692
@@ -0,0 +1,284 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { syntaxTree } from "@codemirror/language";
4
+ import type { EditorState } from "@codemirror/state";
5
+ import type { SyntaxNode, Tree } from "@lezer/common";
6
+ import {
7
+ NON_CODE_NODES,
8
+ PyKeyword,
9
+ PyNode,
10
+ SCOPE_CREATING_NODES,
11
+ } from "../python-node-names";
12
+ import { hasSyntaxErrors } from "../reactive-references/analyzer";
13
+
14
+ export interface CodeLensTarget {
15
+ from: number;
16
+ to: number;
17
+ name: string;
18
+ }
19
+
20
+ /**
21
+ * Finds the first module-scope assignment site for each name in `names`.
22
+ */
23
+ export function findDeclarationSites(options: {
24
+ state: EditorState;
25
+ names: ReadonlySet<string>;
26
+ }): CodeLensTarget[] {
27
+ const { state, names } = options;
28
+ if (names.size === 0) {
29
+ return [];
30
+ }
31
+ const tree = syntaxTree(state);
32
+ if (!tree || hasSyntaxErrors(tree)) {
33
+ return [];
34
+ }
35
+
36
+ const firstSites = new Map<string, CodeLensTarget>();
37
+
38
+ const collectTargetNames = (node: SyntaxNode) => {
39
+ if (node.name === PyNode.VariableName) {
40
+ const name = state.doc.sliceString(node.from, node.to);
41
+ if (names.has(name) && !firstSites.has(name)) {
42
+ firstSites.set(name, { from: node.from, to: node.to, name });
43
+ }
44
+ return;
45
+ }
46
+ // Recurse into tuple/list unpacking, e.g. `(a, b) = ...` or `[a, b] = ...`,
47
+ // and parenthesized targets, e.g. `(df) = ...`.
48
+ // Member/subscript targets (`obj.attr = ...`) are not declarations.
49
+ if (
50
+ node.name === PyNode.TupleExpression ||
51
+ node.name === PyNode.ArrayExpression ||
52
+ node.name === PyNode.ParenthesizedExpression
53
+ ) {
54
+ for (let child = node.firstChild; child; child = child.nextSibling) {
55
+ collectTargetNames(child);
56
+ }
57
+ }
58
+ };
59
+
60
+ const collectAssignmentTargets = (assign: SyntaxNode) => {
61
+ // Targets are everything before the last AssignOp (handles `a = b = ...`).
62
+ // A bare annotation (`x: int`) has no AssignOp and is skipped.
63
+ let lastAssignOp = -1;
64
+ for (let child = assign.firstChild; child; child = child.nextSibling) {
65
+ if (child.name === PyNode.AssignOp) {
66
+ lastAssignOp = child.from;
67
+ }
68
+ }
69
+ if (lastAssignOp === -1) {
70
+ return;
71
+ }
72
+ for (
73
+ let child = assign.firstChild;
74
+ child && child.from < lastAssignOp;
75
+ child = child.nextSibling
76
+ ) {
77
+ collectTargetNames(child);
78
+ }
79
+ };
80
+
81
+ const visit = (node: SyntaxNode) => {
82
+ // Only module-scope assignments count as creation sites; top-level
83
+ // `if`/`for`/`try` bodies are still module scope.
84
+ if (SCOPE_CREATING_NODES.has(node.name)) {
85
+ return;
86
+ }
87
+ if (node.name === PyNode.AssignStatement) {
88
+ collectAssignmentTargets(node);
89
+ return;
90
+ }
91
+ for (let child = node.firstChild; child; child = child.nextSibling) {
92
+ visit(child);
93
+ }
94
+ };
95
+ visit(tree.topNode);
96
+
97
+ return [...firstSites.values()];
98
+ }
99
+
100
+ // Note that the cache pattern assumes users `import marimo as mo`.
101
+ // This is a common convention for marimo notebooks.
102
+ const CACHE_PATTERN = /\bmo\.(?:persistent_cache|cache)\b/g;
103
+
104
+ export interface CacheSite {
105
+ from: number;
106
+ to: number;
107
+ /**
108
+ * Variable the cache is bound to: the decorated function name
109
+ * (`@mo.cache def f`), the assignment target (`g = mo.cache(f)`), or the
110
+ * `as` binding (`with mo.persistent_cache("k") as c`).
111
+ */
112
+ boundName: string | null;
113
+ /** First string argument, e.g. the persistent_cache name. */
114
+ cacheName: string | null;
115
+ }
116
+
117
+ function stripStringQuotes(text: string): string {
118
+ const match = text.match(/^[A-Za-z]*("""|'''|"|')([\s\S]*)\1$/);
119
+ return match ? match[2] : text;
120
+ }
121
+
122
+ /**
123
+ * Extracts a static cache name from the first positional argument or an
124
+ * explicit `name=` argument. Other string-valued options, such as
125
+ * `hash_type="content"` and `save_path="cache"`, are not cache names.
126
+ */
127
+ function findStaticCacheName(
128
+ argList: SyntaxNode | null,
129
+ text: (node: SyntaxNode) => string,
130
+ ): string | null {
131
+ if (!argList) {
132
+ return null;
133
+ }
134
+
135
+ const isArgumentBoundary = (node: SyntaxNode | null) =>
136
+ node?.name === "," || node?.name === ")";
137
+
138
+ let firstArgument: SyntaxNode | null = null;
139
+ for (let child = argList.firstChild; child; child = child.nextSibling) {
140
+ if (child.name === "(" || child.name === ")" || child.name === ",") {
141
+ continue;
142
+ }
143
+ firstArgument ??= child;
144
+
145
+ if (
146
+ child.name === PyNode.VariableName &&
147
+ text(child) === "name" &&
148
+ child.nextSibling?.name === PyNode.AssignOp
149
+ ) {
150
+ const value = child.nextSibling.nextSibling;
151
+ if (
152
+ value?.name === PyNode.String &&
153
+ isArgumentBoundary(value.nextSibling)
154
+ ) {
155
+ return stripStringQuotes(text(value));
156
+ }
157
+ }
158
+ }
159
+
160
+ if (!firstArgument) {
161
+ return null;
162
+ }
163
+ if (
164
+ firstArgument.name === PyNode.String &&
165
+ isArgumentBoundary(firstArgument.nextSibling)
166
+ ) {
167
+ return stripStringQuotes(text(firstArgument));
168
+ }
169
+ return null;
170
+ }
171
+
172
+ /**
173
+ * Resolves a `mo.cache` / `mo.persistent_cache` match into a CacheSite:
174
+ * extends `to` past call arguments (`mo.cache(pin_modules=True)`), so an
175
+ * icon placed at `to` lands after the closing paren, and extracts the bound
176
+ * variable name and the cache-name string argument.
177
+ */
178
+ function analyzeCacheSite(
179
+ state: EditorState,
180
+ tree: Tree,
181
+ from: number,
182
+ to: number,
183
+ ): CacheSite {
184
+ const text = (node: SyntaxNode) => state.doc.sliceString(node.from, node.to);
185
+ const node = tree.resolveInner(from, 1);
186
+ let boundName: string | null = null;
187
+ let argList: SyntaxNode | null = null;
188
+ let end = to;
189
+
190
+ // `mo.cache(...)` as an expression wraps in a CallExpression that starts
191
+ // at the match
192
+ let call: SyntaxNode | null = null;
193
+ for (
194
+ let ancestor: SyntaxNode | null = node.parent;
195
+ ancestor && ancestor.from === from;
196
+ ancestor = ancestor.parent
197
+ ) {
198
+ if (ancestor.name === PyNode.CallExpression) {
199
+ call = ancestor;
200
+ break;
201
+ }
202
+ }
203
+
204
+ if (call) {
205
+ end = call.to;
206
+ argList = call.getChild(PyNode.ArgList);
207
+ const parent = call.parent;
208
+ if (parent?.name === PyNode.AssignStatement) {
209
+ // `g = mo.cache(f)` — the direct VariableName child is the target
210
+ const target = parent.getChild(PyNode.VariableName);
211
+ boundName = target && target.from < call.from ? text(target) : null;
212
+ } else if (parent?.name === PyNode.WithStatement) {
213
+ // `with mo.persistent_cache("k") as c:` — `as c` follows the call
214
+ let child = parent.firstChild;
215
+ while (child) {
216
+ if (child.from < call.to || child.name === PyKeyword.As) {
217
+ child = child.nextSibling;
218
+ continue;
219
+ }
220
+ if (child.name === PyNode.VariableName) {
221
+ boundName = text(child);
222
+ }
223
+ break;
224
+ }
225
+ }
226
+ } else if (node.parent?.name === PyNode.Decorator) {
227
+ // `@mo.cache(...)` decorators are flat: the ArgList is a direct sibling
228
+ const decorator = node.parent;
229
+ const decoratorArgs = decorator.getChild(PyNode.ArgList);
230
+ if (decoratorArgs?.from === to) {
231
+ argList = decoratorArgs;
232
+ end = decoratorArgs.to;
233
+ }
234
+ // The decorated function's name
235
+ const fn = decorator.parent?.getChild(PyNode.FunctionDefinition);
236
+ const fnName = fn?.getChild(PyNode.VariableName);
237
+ boundName = fnName ? text(fnName) : null;
238
+ }
239
+
240
+ const cacheName = findStaticCacheName(argList, text);
241
+ return { from, to: end, boundName, cacheName };
242
+ }
243
+
244
+ /**
245
+ * True when the `mo` at `from` is a standalone module reference (the object of
246
+ * `mo.cache`), not an attribute of something else. This filters out chains
247
+ * like `obj.mo.cache(f)` — where `mo` is a `PropertyName` rather than a
248
+ * `VariableName` — and mentions inside comments or strings.
249
+ */
250
+ function isStandaloneMoReference(
251
+ state: EditorState,
252
+ tree: Tree,
253
+ from: number,
254
+ ): boolean {
255
+ const node = tree.resolveInner(from, 1);
256
+ if (NON_CODE_NODES.has(node.name)) {
257
+ return false;
258
+ }
259
+ // In `obj.mo.cache`, `mo` parses as a `PropertyName`; a standalone `mo`
260
+ // (including whitespace-separated `mo . cache`) parses as a `VariableName`.
261
+ return (
262
+ node.name === PyNode.VariableName &&
263
+ state.doc.sliceString(node.from, node.to) === "mo"
264
+ );
265
+ }
266
+
267
+ /**
268
+ * Finds occurrences of `mo.cache` / `mo.persistent_cache` (as a decorator,
269
+ * call, or context manager) with a simple text match, skipping mentions in
270
+ * comments, strings, and attribute chains (`obj.mo.cache`). `to` extends past
271
+ * call arguments when present.
272
+ */
273
+ export function findCacheSites(state: EditorState): CacheSite[] {
274
+ const tree = syntaxTree(state);
275
+ if (!tree || hasSyntaxErrors(tree)) {
276
+ return [];
277
+ }
278
+ return [...state.doc.toString().matchAll(CACHE_PATTERN)]
279
+ .filter((match) => isStandaloneMoReference(state, tree, match.index ?? 0))
280
+ .map((match) => {
281
+ const from = match.index ?? 0;
282
+ return analyzeCacheSite(state, tree, from, from + match[0].length);
283
+ });
284
+ }
@@ -0,0 +1,123 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { atom } from "jotai";
4
+ import type { CellId } from "@/core/cells/ids";
5
+ import { dataConnectionsMapAtom } from "@/core/datasets/data-source-connections";
6
+ import { INTERNAL_SQL_ENGINES } from "@/core/datasets/engines";
7
+ import { datasetTablesAtom } from "@/core/datasets/state";
8
+ import { store } from "@/core/state/jotai";
9
+ import { storageNamespacesAtom } from "@/core/storage/state";
10
+ import { variablesAtom } from "@/core/variables/state";
11
+ import type { VariableName } from "@/core/variables/types";
12
+ import { Logger } from "@/utils/Logger";
13
+
14
+ export type CodeLensKind = "table" | "connection" | "bucket" | "cache";
15
+
16
+ export interface CodeLensSpec {
17
+ /** Document position the icon is anchored at */
18
+ pos: number;
19
+ kind: CodeLensKind;
20
+ /** Variable name for entities; a stable identity key for cache sites */
21
+ name: string;
22
+ /** Extra context for cache sites */
23
+ cache?: {
24
+ boundName: string | null;
25
+ cacheName: string | null;
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Sets `name` to `kind`, logging (and overwriting) if the name was already
31
+ * claimed by a different kind. This should be rare.
32
+ */
33
+ function setEntity(
34
+ entities: Map<string, CodeLensKind>,
35
+ name: string,
36
+ kind: CodeLensKind,
37
+ ): void {
38
+ const existing = entities.get(name);
39
+ if (existing !== undefined && existing !== kind) {
40
+ Logger.error(
41
+ `[code-lens] "${name}" is claimed by both a ${existing} and a ${kind} entity; keeping ${kind}`,
42
+ );
43
+ }
44
+ entities.set(name, kind);
45
+ }
46
+
47
+ /**
48
+ * All datasource (dataframe/SQL engine) and storage bucket names in the
49
+ * notebook, keyed by name, independent of any particular cell.
50
+ */
51
+ const lensEntityKindsAtom = atom((get) => {
52
+ const entities = new Map<string, CodeLensKind>();
53
+ for (const table of get(datasetTablesAtom)) {
54
+ if (table.variable_name) {
55
+ setEntity(entities, table.variable_name, "table");
56
+ }
57
+ }
58
+ for (const name of get(dataConnectionsMapAtom).keys()) {
59
+ if (!INTERNAL_SQL_ENGINES.has(name)) {
60
+ setEntity(entities, name, "connection");
61
+ }
62
+ }
63
+ for (const namespace of get(storageNamespacesAtom)) {
64
+ setEntity(entities, namespace.name, "bucket");
65
+ }
66
+ return entities;
67
+ });
68
+
69
+ interface LensEntitiesByCell {
70
+ byCell: Map<CellId, Map<string, CodeLensKind>>;
71
+ /**
72
+ * Entities the kernel hasn't reported a variable for yet (still executing,
73
+ * or just typed), so they can't be attributed to a declaring cell.
74
+ */
75
+ pending: Map<string, CodeLensKind>;
76
+ }
77
+
78
+ /**
79
+ * Groups `lensEntityKindsAtom` by declaring cell, once per store update.
80
+ * With C cells and T datasource/bucket entities, this turns
81
+ * an O(C x T) per-round cost (every cell re-scanning every entity) into O(T) here.
82
+ */
83
+ const lensEntitiesByCellAtom = atom((get): LensEntitiesByCell => {
84
+ const kinds = get(lensEntityKindsAtom);
85
+ const variables = get(variablesAtom);
86
+ const byCell = new Map<CellId, Map<string, CodeLensKind>>();
87
+ const pending = new Map<string, CodeLensKind>();
88
+
89
+ for (const [name, kind] of kinds) {
90
+ const variable = variables[name as VariableName];
91
+ if (variable == null) {
92
+ pending.set(name, kind);
93
+ continue;
94
+ }
95
+ for (const declaringCell of variable.declaredBy) {
96
+ let forCell = byCell.get(declaringCell);
97
+ if (!forCell) {
98
+ forCell = new Map();
99
+ byCell.set(declaringCell, forCell);
100
+ }
101
+ forCell.set(name, kind);
102
+ }
103
+ }
104
+
105
+ return { byCell, pending };
106
+ });
107
+
108
+ /**
109
+ * Variables declared in `cellId` that are datasources (dataframes and SQL
110
+ * engines) or storage buckets, keyed by variable name.
111
+ */
112
+ export function getLensEntities(cellId: CellId): Map<string, CodeLensKind> {
113
+ const { byCell, pending } = store.get(lensEntitiesByCellAtom);
114
+ const entities = new Map(byCell.get(cellId));
115
+ // Be permissive when the kernel hasn't reported a variable yet: any cell
116
+ // could turn out to be the declaring one.
117
+ for (const [name, kind] of pending) {
118
+ if (!entities.has(name)) {
119
+ entities.set(name, kind);
120
+ }
121
+ }
122
+ return entities;
123
+ }
@@ -0,0 +1,298 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { type Extension, StateEffect, StateField } from "@codemirror/state";
4
+ import {
5
+ Decoration,
6
+ type DecorationSet,
7
+ EditorView,
8
+ showTooltip,
9
+ type Tooltip,
10
+ ViewPlugin,
11
+ type ViewUpdate,
12
+ WidgetType,
13
+ } from "@codemirror/view";
14
+ import { type DebouncedFunc, debounce } from "lodash-es";
15
+ import type { CellId } from "@/core/cells/ids";
16
+ import { getFeatureFlag } from "@/core/config/feature-flag";
17
+ import { dataConnectionsMapAtom } from "@/core/datasets/data-source-connections";
18
+ import { datasetTablesAtom } from "@/core/datasets/state";
19
+ import { store } from "@/core/state/jotai";
20
+ import { storageNamespacesAtom } from "@/core/storage/state";
21
+ import { variablesAtom } from "@/core/variables/state";
22
+ import { languageAdapterState } from "../language/extension";
23
+ import { openLensTarget } from "./actions";
24
+ import { findCacheSites, findDeclarationSites } from "./analyzer";
25
+ import { type CodeLensSpec, getLensEntities } from "./entities";
26
+ import { LENS_ICONS, LENS_TOOLTIPS } from "./icons";
27
+ import { mountLensPopover } from "./popover";
28
+
29
+ // Delay (in ms) before showing the hover popover, matching the app's
30
+ // tooltip delay
31
+ export const CODE_LENS_HOVER_DELAY_MS = 400;
32
+
33
+ const setCodeLenses = StateEffect.define<CodeLensSpec[]>();
34
+ const setHoveredLens = StateEffect.define<CodeLensSpec | null>();
35
+
36
+ // Pending hover timers, keyed by widget element so `destroy` can cancel them
37
+ const HOVER_TIMERS = new WeakMap<HTMLElement, number>();
38
+
39
+ class CodeLensWidget extends WidgetType {
40
+ private readonly spec: CodeLensSpec;
41
+
42
+ constructor(spec: CodeLensSpec) {
43
+ super();
44
+ this.spec = spec;
45
+ }
46
+
47
+ override eq(other: CodeLensWidget): boolean {
48
+ return (
49
+ // `pos` is captured by the DOM hover/click handlers, so a reused widget
50
+ // whose anchor moved must not be treated as equal
51
+ this.spec.pos === other.spec.pos &&
52
+ this.spec.kind === other.spec.kind &&
53
+ this.spec.name === other.spec.name &&
54
+ this.spec.cache?.boundName === other.spec.cache?.boundName &&
55
+ this.spec.cache?.cacheName === other.spec.cache?.cacheName
56
+ );
57
+ }
58
+
59
+ override toDOM(view: EditorView): HTMLElement {
60
+ const { spec } = this;
61
+ const element = document.createElement("span");
62
+ element.className = "mo-code-lens";
63
+ element.setAttribute("role", "button");
64
+ // Focusable so the action is reachable and activatable by keyboard
65
+ element.tabIndex = 0;
66
+ // No `title`: the native tooltip is replaced by the hover popover
67
+ element.setAttribute("aria-label", LENS_TOOLTIPS[spec.kind]);
68
+ // Static, trusted markup (see icons.ts)
69
+ element.innerHTML = LENS_ICONS[spec.kind];
70
+ const hidePopover = () => {
71
+ window.clearTimeout(HOVER_TIMERS.get(element));
72
+ HOVER_TIMERS.delete(element);
73
+ if (view.state.field(codeLensHoverField, false)) {
74
+ view.dispatch({ effects: setHoveredLens.of(null) });
75
+ }
76
+ };
77
+ element.onmouseenter = () => {
78
+ const timer = window.setTimeout(() => {
79
+ view.dispatch({ effects: setHoveredLens.of(spec) });
80
+ }, CODE_LENS_HOVER_DELAY_MS);
81
+ HOVER_TIMERS.set(element, timer);
82
+ };
83
+ element.onmouseleave = hidePopover;
84
+ element.onmousemove = (event) => {
85
+ // Keep the editor's built-in hover documentation tooltip from also
86
+ // triggering over the icon
87
+ event.stopPropagation();
88
+ };
89
+ element.onmousedown = (event) => {
90
+ // Don't move the cursor or steal focus from the editor
91
+ event.preventDefault();
92
+ event.stopPropagation();
93
+ };
94
+ element.onclick = (event) => {
95
+ event.preventDefault();
96
+ event.stopPropagation();
97
+ hidePopover();
98
+ openLensTarget(spec.kind);
99
+ };
100
+ element.onkeydown = (event) => {
101
+ if (event.key === "Enter" || event.key === " ") {
102
+ event.preventDefault();
103
+ event.stopPropagation();
104
+ hidePopover();
105
+ openLensTarget(spec.kind);
106
+ }
107
+ };
108
+ return element;
109
+ }
110
+
111
+ override destroy(dom: HTMLElement): void {
112
+ window.clearTimeout(HOVER_TIMERS.get(dom));
113
+ HOVER_TIMERS.delete(dom);
114
+ }
115
+
116
+ override ignoreEvent(): boolean {
117
+ // The widget handles its own events
118
+ return true;
119
+ }
120
+ }
121
+
122
+ function createLensTooltip(spec: CodeLensSpec): Tooltip {
123
+ return {
124
+ pos: spec.pos,
125
+ above: true,
126
+ create: () => {
127
+ const dom = document.createElement("div");
128
+ // Same chrome as the SQL completion/hover popovers
129
+ dom.classList.add("mo-cm-tooltip", "docs-documentation");
130
+ const unmount = mountLensPopover(dom, spec);
131
+ return { dom, resize: false, destroy: unmount };
132
+ },
133
+ };
134
+ }
135
+
136
+ const codeLensHoverField = StateField.define<Tooltip | null>({
137
+ create() {
138
+ return null;
139
+ },
140
+ update(tooltip, tr) {
141
+ for (const effect of tr.effects) {
142
+ if (effect.is(setHoveredLens)) {
143
+ return effect.value ? createLensTooltip(effect.value) : null;
144
+ }
145
+ if (effect.is(setCodeLenses)) {
146
+ // Lenses were rebuilt; the hovered widget may be gone
147
+ return null;
148
+ }
149
+ }
150
+ return tr.docChanged ? null : tooltip;
151
+ },
152
+ provide: (field) => showTooltip.from(field),
153
+ });
154
+
155
+ const codeLensField = StateField.define<DecorationSet>({
156
+ create() {
157
+ return Decoration.none;
158
+ },
159
+ update(decorations, tr) {
160
+ decorations = decorations.map(tr.changes);
161
+ for (const effect of tr.effects) {
162
+ if (effect.is(setCodeLenses)) {
163
+ decorations = Decoration.set(
164
+ effect.value.map((spec) =>
165
+ Decoration.widget({
166
+ widget: new CodeLensWidget(spec),
167
+ side: 1,
168
+ }).range(spec.pos),
169
+ ),
170
+ // Variable and cache lenses are collected separately, so sort
171
+ true,
172
+ );
173
+ }
174
+ }
175
+ return decorations;
176
+ },
177
+ provide: (field) => EditorView.decorations.from(field),
178
+ });
179
+
180
+ /**
181
+ * Plugin that keeps code lens decorations in sync with the document and the
182
+ * datasource/bucket state.
183
+ */
184
+ class CodeLensPlugin {
185
+ private readonly view: EditorView;
186
+ private readonly cellId: CellId;
187
+ private readonly includeCache: boolean;
188
+ private readonly unsubscribes: Array<() => void>;
189
+
190
+ // Delay (in ms) before recomputing lenses after user changes or store updates
191
+ private readonly debounceMs = 300;
192
+ private readonly scheduleUpdate: DebouncedFunc<() => void>;
193
+
194
+ constructor(view: EditorView, cellId: CellId, includeCache: boolean) {
195
+ this.view = view;
196
+ this.cellId = cellId;
197
+ this.includeCache = includeCache;
198
+ this.scheduleUpdate = debounce(() => this.run(), this.debounceMs);
199
+ const onStoreChange = () => this.scheduleUpdate();
200
+ this.unsubscribes = [
201
+ store.sub(datasetTablesAtom, onStoreChange),
202
+ store.sub(dataConnectionsMapAtom, onStoreChange),
203
+ store.sub(storageNamespacesAtom, onStoreChange),
204
+ // The declaring-cell filter depends on variables
205
+ store.sub(variablesAtom, onStoreChange),
206
+ ];
207
+ this.scheduleUpdate();
208
+ }
209
+
210
+ update(update: ViewUpdate) {
211
+ // Recompute on edits, and when the cell's language changes (e.g. Python ->
212
+ // SQL) so stale Python-only icons are cleared even if the text is unchanged
213
+ const adapterChanged =
214
+ update.startState.field(languageAdapterState, false)?.type !==
215
+ update.state.field(languageAdapterState, false)?.type;
216
+ if (update.docChanged || adapterChanged) {
217
+ this.scheduleUpdate();
218
+ }
219
+ }
220
+
221
+ destroy() {
222
+ this.scheduleUpdate.cancel();
223
+ for (const unsubscribe of this.unsubscribes) {
224
+ unsubscribe();
225
+ }
226
+ }
227
+
228
+ private run() {
229
+ const { state } = this.view;
230
+ const lenses: CodeLensSpec[] = [];
231
+
232
+ // Only python cells: SQL/markdown docs aren't python, and a cache icon
233
+ // inside a SQL string would be misleading.
234
+ const adapterType = state.field(languageAdapterState, false)?.type;
235
+ if (adapterType == null || adapterType === "python") {
236
+ const entities = getLensEntities(this.cellId);
237
+ const targets = findDeclarationSites({
238
+ state,
239
+ names: new Set(entities.keys()),
240
+ });
241
+ for (const target of targets) {
242
+ const kind = entities.get(target.name);
243
+ if (kind) {
244
+ lenses.push({ pos: target.to, kind, name: target.name });
245
+ }
246
+ }
247
+ if (this.includeCache) {
248
+ for (const site of findCacheSites(state)) {
249
+ lenses.push({
250
+ pos: site.to,
251
+ kind: "cache",
252
+ name: `cache:${site.from}`,
253
+ cache: { boundName: site.boundName, cacheName: site.cacheName },
254
+ });
255
+ }
256
+ }
257
+ }
258
+
259
+ // Defer dispatch to avoid triggering during an editor update cycle
260
+ queueMicrotask(() => {
261
+ this.view.dispatch({ effects: setCodeLenses.of(lenses) });
262
+ });
263
+ }
264
+ }
265
+
266
+ const codeLensTheme = EditorView.baseTheme({
267
+ ".mo-code-lens": {
268
+ display: "inline-flex",
269
+ verticalAlign: "baseline",
270
+ // Optically center the icon against the text
271
+ transform: "translateY(1.5px)",
272
+ marginLeft: "0.3em",
273
+ cursor: "pointer",
274
+ opacity: "0.5",
275
+ },
276
+ ".mo-code-lens:hover": {
277
+ opacity: "1",
278
+ },
279
+ });
280
+
281
+ /**
282
+ * Inline icons linking datasource/bucket variables and `mo.cache` /
283
+ * `mo.persistent_cache` calls to their panels.
284
+ * Gated behind the `editor_code_lens` feature flag.
285
+ */
286
+ export function codeLensBundle(cellId: CellId): Extension {
287
+ if (!getFeatureFlag("editor_code_lens")) {
288
+ return [];
289
+ }
290
+ return [
291
+ codeLensField,
292
+ codeLensHoverField,
293
+ ViewPlugin.define(
294
+ (view) => new CodeLensPlugin(view, cellId, getFeatureFlag("cache_panel")),
295
+ ),
296
+ codeLensTheme,
297
+ ];
298
+ }