@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,325 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { python } from "@codemirror/lang-python";
4
+ import { EditorState } from "@codemirror/state";
5
+ import { EditorView } from "@codemirror/view";
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { cellId, variableName } from "@/__tests__/branded";
8
+ import { languageAdapterState } from "@/core/codemirror/language/extension";
9
+ import { LanguageAdapters } from "@/core/codemirror/language/LanguageAdapters";
10
+ import type { ExperimentalFeatures } from "@/core/config/feature-flag";
11
+ import { getFeatureFlag } from "@/core/config/feature-flag";
12
+ import {
13
+ type DataSourceConnection,
14
+ dataSourceConnectionsAtom,
15
+ } from "@/core/datasets/data-source-connections";
16
+ import { type ConnectionName, DUCKDB_ENGINE } from "@/core/datasets/engines";
17
+ import { datasetsAtom } from "@/core/datasets/state";
18
+ import type { QualifiedColumn } from "@/core/datasets/types";
19
+ import type { DataTable } from "@/core/kernel/messages";
20
+ import { store } from "@/core/state/jotai";
21
+ import { storageAtom } from "@/core/storage/state";
22
+ import type { StorageNamespace } from "@/core/storage/types";
23
+ import { variablesAtom } from "@/core/variables/state";
24
+ import type { Variables } from "@/core/variables/types";
25
+ import { openLensTarget } from "../actions";
26
+ import { CODE_LENS_HOVER_DELAY_MS, codeLensBundle } from "../extension";
27
+ import { mountLensPopover } from "../popover";
28
+
29
+ vi.mock("@/core/config/feature-flag", () => ({
30
+ getFeatureFlag: vi.fn(),
31
+ }));
32
+
33
+ vi.mock("../actions", () => ({
34
+ openLensTarget: vi.fn(),
35
+ }));
36
+
37
+ vi.mock("../popover", () => ({
38
+ mountLensPopover: vi.fn(() => vi.fn()),
39
+ }));
40
+
41
+ function mockFlags(flags: Partial<ExperimentalFeatures>) {
42
+ vi.mocked(getFeatureFlag).mockImplementation((flag) => flags[flag] ?? false);
43
+ }
44
+
45
+ const DF_TABLE: DataTable = {
46
+ name: "df",
47
+ source: "memory",
48
+ source_type: "local",
49
+ num_rows: 1,
50
+ num_columns: 1,
51
+ columns: [],
52
+ variable_name: variableName("df"),
53
+ };
54
+
55
+ const BUCKET_NAMESPACE: StorageNamespace = {
56
+ name: variableName("bucket"),
57
+ displayName: "bucket",
58
+ protocol: "s3",
59
+ rootPath: "s3://bucket",
60
+ backendType: "obstore",
61
+ storageEntries: [],
62
+ };
63
+
64
+ const ENGINE_CONNECTION: DataSourceConnection = {
65
+ name: "engine" as ConnectionName,
66
+ source: "postgres",
67
+ dialect: "postgresql",
68
+ display_name: "engine (postgres)",
69
+ databases: [],
70
+ };
71
+
72
+ function seedStore(opts: {
73
+ tables?: DataTable[];
74
+ namespaces?: StorageNamespace[];
75
+ connections?: DataSourceConnection[];
76
+ variables?: Variables;
77
+ }) {
78
+ store.set(datasetsAtom, {
79
+ tables: opts.tables ?? [],
80
+ expandedTables: new Set<string>(),
81
+ expandedColumns: new Set<QualifiedColumn>(),
82
+ columnsPreviews: new Map(),
83
+ });
84
+ store.set(storageAtom, {
85
+ namespaces: opts.namespaces ?? [],
86
+ entriesByPath: new Map(),
87
+ pageMetadataByPath: new Map(),
88
+ });
89
+ store.set(dataSourceConnectionsAtom, {
90
+ latestEngineSelected: DUCKDB_ENGINE,
91
+ connectionsMap: new Map(
92
+ (opts.connections ?? []).map((connection) => [
93
+ connection.name,
94
+ connection,
95
+ ]),
96
+ ),
97
+ });
98
+ store.set(variablesAtom, opts.variables ?? {});
99
+ }
100
+
101
+ describe("codeLensBundle", () => {
102
+ let view: EditorView | null = null;
103
+
104
+ beforeEach(() => {
105
+ vi.useFakeTimers();
106
+ mockFlags({ editor_code_lens: true, cache_panel: true });
107
+ seedStore({});
108
+ });
109
+
110
+ afterEach(() => {
111
+ if (view) {
112
+ view.destroy();
113
+ view = null;
114
+ }
115
+ document.body.innerHTML = "";
116
+ vi.useRealTimers();
117
+ vi.clearAllMocks();
118
+ });
119
+
120
+ async function mount(
121
+ code: string,
122
+ language: "python" | "sql" | "markdown" = "python",
123
+ ): Promise<EditorView> {
124
+ const state = EditorState.create({
125
+ doc: code,
126
+ extensions: [
127
+ python(),
128
+ languageAdapterState.init(() => LanguageAdapters[language]),
129
+ codeLensBundle(cellId("cell1")),
130
+ ],
131
+ });
132
+ view = new EditorView({ state, parent: document.body });
133
+ // Fire the debounced analysis, then flush the deferred dispatch
134
+ vi.advanceTimersByTime(300);
135
+ await Promise.resolve();
136
+ view.dispatch({});
137
+ return view;
138
+ }
139
+
140
+ function lenses(v: EditorView): HTMLElement[] {
141
+ return [...v.dom.querySelectorAll<HTMLElement>(".mo-code-lens")];
142
+ }
143
+
144
+ it("returns an empty extension when the flag is off", () => {
145
+ mockFlags({ editor_code_lens: false });
146
+ expect(codeLensBundle(cellId("cell1"))).toEqual([]);
147
+ });
148
+
149
+ it("renders a lens at a dataframe declaration", async () => {
150
+ seedStore({ tables: [DF_TABLE] });
151
+ const v = await mount("df = load()\nx = 1");
152
+
153
+ const found = lenses(v);
154
+ expect(found).toHaveLength(1);
155
+ expect(found[0].getAttribute("aria-label")).toBe("Open in data sources");
156
+ // The native tooltip is replaced by the hover popover
157
+ expect(found[0].getAttribute("title")).toBeNull();
158
+ });
159
+
160
+ it("opens the datasources panel on click", async () => {
161
+ seedStore({ tables: [DF_TABLE] });
162
+ const v = await mount("df = load()");
163
+
164
+ lenses(v)[0].dispatchEvent(new MouseEvent("click", { bubbles: true }));
165
+
166
+ expect(openLensTarget).toHaveBeenCalledWith("table");
167
+ });
168
+
169
+ it("is keyboard focusable and activatable", async () => {
170
+ seedStore({ tables: [DF_TABLE] });
171
+ const v = await mount("df = load()");
172
+ const lens = lenses(v)[0];
173
+
174
+ expect(lens.getAttribute("role")).toBe("button");
175
+ expect(lens.tabIndex).toBe(0);
176
+
177
+ lens.dispatchEvent(
178
+ new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
179
+ );
180
+ expect(openLensTarget).toHaveBeenCalledWith("table");
181
+
182
+ lens.dispatchEvent(
183
+ new KeyboardEvent("keydown", { key: " ", bubbles: true }),
184
+ );
185
+ expect(openLensTarget).toHaveBeenCalledTimes(2);
186
+ });
187
+
188
+ it("renders a lens at a bucket declaration", async () => {
189
+ seedStore({ namespaces: [BUCKET_NAMESPACE] });
190
+ const v = await mount("bucket = get_bucket()");
191
+
192
+ const found = lenses(v);
193
+ expect(found).toHaveLength(1);
194
+ expect(found[0].getAttribute("aria-label")).toBe("Open in remote storage");
195
+
196
+ found[0].dispatchEvent(new MouseEvent("click", { bubbles: true }));
197
+ expect(openLensTarget).toHaveBeenCalledWith("bucket");
198
+ });
199
+
200
+ it("renders a lens at a connection declaration", async () => {
201
+ seedStore({ connections: [ENGINE_CONNECTION] });
202
+ const v = await mount("engine = create_engine()");
203
+
204
+ const found = lenses(v);
205
+ expect(found).toHaveLength(1);
206
+ expect(found[0].getAttribute("aria-label")).toBe("Open in data sources");
207
+
208
+ found[0].dispatchEvent(new MouseEvent("click", { bubbles: true }));
209
+ expect(openLensTarget).toHaveBeenCalledWith("connection");
210
+ });
211
+
212
+ it("renders a lens at mo.cache and mo.persistent_cache sites", async () => {
213
+ const v = await mount(
214
+ '@mo.cache\ndef f():\n return 1\n\nwith mo.persistent_cache("k"):\n pass',
215
+ );
216
+
217
+ const found = lenses(v);
218
+ expect(found).toHaveLength(2);
219
+ expect(found[0].getAttribute("aria-label")).toBe("Open cache panel");
220
+
221
+ found[0].dispatchEvent(new MouseEvent("click", { bubbles: true }));
222
+ expect(openLensTarget).toHaveBeenCalledWith("cache");
223
+ });
224
+
225
+ it("shows a popover on hover and hides it on leave", async () => {
226
+ seedStore({ tables: [DF_TABLE] });
227
+ const v = await mount("df = load()");
228
+ const lens = lenses(v)[0];
229
+
230
+ lens.dispatchEvent(new MouseEvent("mouseenter"));
231
+ vi.advanceTimersByTime(CODE_LENS_HOVER_DELAY_MS);
232
+
233
+ expect(v.dom.querySelector(".mo-cm-tooltip")).not.toBeNull();
234
+ expect(mountLensPopover).toHaveBeenCalledWith(
235
+ expect.any(HTMLElement),
236
+ expect.objectContaining({ kind: "table", name: "df" }),
237
+ );
238
+
239
+ lens.dispatchEvent(new MouseEvent("mouseleave"));
240
+ expect(v.dom.querySelector(".mo-cm-tooltip")).toBeNull();
241
+ });
242
+
243
+ it("does not show a popover before the hover delay", async () => {
244
+ seedStore({ tables: [DF_TABLE] });
245
+ const v = await mount("df = load()");
246
+ const lens = lenses(v)[0];
247
+
248
+ lens.dispatchEvent(new MouseEvent("mouseenter"));
249
+ vi.advanceTimersByTime(CODE_LENS_HOVER_DELAY_MS - 100);
250
+ lens.dispatchEvent(new MouseEvent("mouseleave"));
251
+ vi.advanceTimersByTime(CODE_LENS_HOVER_DELAY_MS);
252
+
253
+ expect(v.dom.querySelector(".mo-cm-tooltip")).toBeNull();
254
+ });
255
+
256
+ it("passes cache context to the popover", async () => {
257
+ const v = await mount(
258
+ '@mo.cache\ndef add(a, b):\n return a + b\n\nwith mo.persistent_cache("k"):\n pass',
259
+ );
260
+ const [decoratorLens, withLens] = lenses(v);
261
+
262
+ decoratorLens.dispatchEvent(new MouseEvent("mouseenter"));
263
+ vi.advanceTimersByTime(CODE_LENS_HOVER_DELAY_MS);
264
+ expect(mountLensPopover).toHaveBeenCalledWith(
265
+ expect.any(HTMLElement),
266
+ expect.objectContaining({
267
+ kind: "cache",
268
+ cache: { boundName: "add", cacheName: null },
269
+ }),
270
+ );
271
+
272
+ decoratorLens.dispatchEvent(new MouseEvent("mouseleave"));
273
+ withLens.dispatchEvent(new MouseEvent("mouseenter"));
274
+ vi.advanceTimersByTime(CODE_LENS_HOVER_DELAY_MS);
275
+ expect(mountLensPopover).toHaveBeenCalledWith(
276
+ expect.any(HTMLElement),
277
+ expect.objectContaining({
278
+ kind: "cache",
279
+ cache: { boundName: null, cacheName: "k" },
280
+ }),
281
+ );
282
+ });
283
+
284
+ it.each(["sql", "markdown"] as const)(
285
+ "does not render lenses in %s cells",
286
+ async (language) => {
287
+ // A `mo.cache` inside a SQL string (or markdown) must not get an icon.
288
+ seedStore({ tables: [DF_TABLE] });
289
+ const v = await mount("df = load()\nmo.cache(f)", language);
290
+
291
+ expect(lenses(v)).toHaveLength(0);
292
+ },
293
+ );
294
+
295
+ it("does not render cache lenses when the cache panel is disabled", async () => {
296
+ mockFlags({ editor_code_lens: true, cache_panel: false });
297
+ const v = await mount("@mo.cache\ndef f():\n return 1");
298
+
299
+ expect(lenses(v)).toHaveLength(0);
300
+ });
301
+
302
+ it("only decorates the declaring cell", async () => {
303
+ const dfName = variableName("df");
304
+ seedStore({
305
+ tables: [DF_TABLE],
306
+ variables: {
307
+ [dfName]: {
308
+ name: dfName,
309
+ declaredBy: [cellId("other-cell")],
310
+ usedBy: [cellId("cell1")],
311
+ },
312
+ },
313
+ });
314
+ const v = await mount("df = load()");
315
+
316
+ expect(lenses(v)).toHaveLength(0);
317
+ });
318
+
319
+ it("decorates only the declaration site, not later references", async () => {
320
+ seedStore({ tables: [DF_TABLE] });
321
+ const v = await mount("df = load()\ndf.head()");
322
+
323
+ expect(lenses(v)).toHaveLength(1);
324
+ });
325
+ });
@@ -0,0 +1,121 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /**
4
+ * Regression guards for code-lens hot paths: assert wall time stays within a
5
+ * generous budget for a large, synthetic notebook.
6
+ */
7
+
8
+ import { python } from "@codemirror/lang-python";
9
+ import { EditorState } from "@codemirror/state";
10
+ import { describe, expect, it } from "vitest";
11
+ import { cellId, variableName } from "@/__tests__/branded";
12
+ import type { CellId } from "@/core/cells/ids";
13
+ import { dataSourceConnectionsAtom } from "@/core/datasets/data-source-connections";
14
+ import { DUCKDB_ENGINE } from "@/core/datasets/engines";
15
+ import { datasetsAtom } from "@/core/datasets/state";
16
+ import type { QualifiedColumn } from "@/core/datasets/types";
17
+ import type { DataTable } from "@/core/kernel/messages";
18
+ import { store } from "@/core/state/jotai";
19
+ import { storageAtom } from "@/core/storage/state";
20
+ import { variablesAtom } from "@/core/variables/state";
21
+ import type { Variables } from "@/core/variables/types";
22
+ import { findCacheSites, findDeclarationSites } from "../analyzer";
23
+ import { getLensEntities } from "../entities";
24
+
25
+ function pythonState(code: string): EditorState {
26
+ return EditorState.create({ doc: code, extensions: [python()] });
27
+ }
28
+
29
+ describe("code-lens performance", () => {
30
+ it("getLensEntities stays fast across many cells in a large notebook", () => {
31
+ const DATASOURCE_COUNT = 2000;
32
+ const CELL_COUNT = 1000;
33
+
34
+ const tables: DataTable[] = [];
35
+ const variables: Variables = {};
36
+ for (let i = 0; i < DATASOURCE_COUNT; i++) {
37
+ const name = variableName(`table_${i}`);
38
+ tables.push({
39
+ name,
40
+ source: "memory",
41
+ source_type: "local",
42
+ num_rows: 1,
43
+ num_columns: 1,
44
+ columns: [],
45
+ variable_name: name,
46
+ });
47
+ // Every datasource is owned by a distinct cell, like a real notebook.
48
+ variables[name] = {
49
+ name,
50
+ declaredBy: [cellId(`owner-${i}`)],
51
+ usedBy: [],
52
+ };
53
+ }
54
+ store.set(datasetsAtom, {
55
+ tables,
56
+ expandedTables: new Set<string>(),
57
+ expandedColumns: new Set<QualifiedColumn>(),
58
+ columnsPreviews: new Map(),
59
+ });
60
+ store.set(storageAtom, {
61
+ namespaces: [],
62
+ entriesByPath: new Map(),
63
+ pageMetadataByPath: new Map(),
64
+ });
65
+ store.set(dataSourceConnectionsAtom, {
66
+ latestEngineSelected: DUCKDB_ENGINE,
67
+ connectionsMap: new Map(),
68
+ });
69
+ store.set(variablesAtom, variables);
70
+
71
+ const cellIds: CellId[] = Array.from({ length: CELL_COUNT }, (_, i) =>
72
+ cellId(`cell-${i}`),
73
+ );
74
+
75
+ const start = performance.now();
76
+ for (const id of cellIds) {
77
+ getLensEntities(id);
78
+ }
79
+ const elapsed = performance.now() - start;
80
+
81
+ // Rescanning all 2000 datasources per cell (instead of an O(1) lookup
82
+ // into a precomputed per-cell index) would take on the order of seconds
83
+ // here; 200ms leaves generous headroom for slow CI while still catching
84
+ // that kind of regression.
85
+ expect(elapsed).toBeLessThan(200);
86
+ });
87
+
88
+ it("findCacheSites stays fast for a large cell, valid or not", () => {
89
+ const lines: string[] = [];
90
+ for (let i = 0; i < 500; i++) {
91
+ lines.push(`@mo.cache`, `def fn_${i}(x):`, ` return x + ${i}`, "");
92
+ }
93
+ const validCode = lines.join("\n");
94
+ // Trailing unclosed paren makes the whole tree unparsable, mimicking a
95
+ // cell mid-edit — this must bail out quickly rather than scanning anyway.
96
+ const brokenCode = `${validCode}\ndf = (`;
97
+
98
+ const start = performance.now();
99
+ findCacheSites(pythonState(validCode));
100
+ findCacheSites(pythonState(brokenCode));
101
+ const elapsed = performance.now() - start;
102
+
103
+ expect(elapsed).toBeLessThan(200);
104
+ });
105
+
106
+ it("findDeclarationSites stays fast for a large cell", () => {
107
+ const lines: string[] = [];
108
+ const names = new Set<string>();
109
+ for (let i = 0; i < 2000; i++) {
110
+ lines.push(`var_${i} = compute_${i}()`);
111
+ names.add(`var_${i}`);
112
+ }
113
+ const state = pythonState(lines.join("\n"));
114
+
115
+ const start = performance.now();
116
+ findDeclarationSites({ state, names });
117
+ const elapsed = performance.now() - start;
118
+
119
+ expect(elapsed).toBeLessThan(200);
120
+ });
121
+ });
@@ -0,0 +1,93 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act } from "@testing-library/react";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import type { CodeLensSpec } from "../entities";
6
+
7
+ // Stable mock reference so every (re)import of popover.tsx after
8
+ // vi.resetModules() sees the same spy — the module-level fetch-throttle
9
+ // state in popover.tsx (`lastCacheInfoFetchAt`) needs to start fresh per
10
+ // test, which requires resetting the whole module, not just the mock.
11
+ const { getCacheInfoMock } = vi.hoisted(() => ({
12
+ getCacheInfoMock: vi.fn().mockResolvedValue(null),
13
+ }));
14
+
15
+ vi.mock("@/core/network/requests", () => ({
16
+ getRequestClient: () => ({ getCacheInfo: getCacheInfoMock }),
17
+ }));
18
+
19
+ const CACHE_SPEC: CodeLensSpec = {
20
+ pos: 0,
21
+ kind: "cache",
22
+ name: "my_cache",
23
+ // `boundName: null` means the popover has no per-cache stats to fall back
24
+ // on, so it always attempts the notebook-wide `getCacheInfo` fetch.
25
+ cache: { boundName: null, cacheName: "my_cache" },
26
+ };
27
+
28
+ describe("CachePopover getCacheInfo throttling", () => {
29
+ let mountLensPopover: typeof import("../popover").mountLensPopover;
30
+ let now: number;
31
+
32
+ beforeEach(async () => {
33
+ vi.resetModules();
34
+ getCacheInfoMock.mockClear();
35
+ getCacheInfoMock.mockResolvedValue(null);
36
+ ({ mountLensPopover } = await import("../popover"));
37
+ // Control `Date.now()` directly (rather than `vi.useFakeTimers()`)
38
+ // so React's own scheduler — which effect-flushing inside `act()`
39
+ // depends on — keeps using real timers.
40
+ now = 0;
41
+ vi.spyOn(Date, "now").mockImplementation(() => now);
42
+ });
43
+
44
+ afterEach(() => {
45
+ document.body.innerHTML = "";
46
+ vi.restoreAllMocks();
47
+ });
48
+
49
+ function mount() {
50
+ const dom = document.createElement("div");
51
+ document.body.append(dom);
52
+ let dispose: () => void;
53
+ act(() => {
54
+ dispose = mountLensPopover(dom, CACHE_SPEC);
55
+ });
56
+ return () => act(() => dispose());
57
+ }
58
+
59
+ it("does not refetch on every hover, only after the throttle interval elapses", () => {
60
+ const disposeFirst = mount();
61
+ expect(getCacheInfoMock).toHaveBeenCalledTimes(1);
62
+ disposeFirst();
63
+
64
+ // Hovering again shortly after should reuse whatever is already in
65
+ // flight/cached rather than firing a second request.
66
+ const disposeSecond = mount();
67
+ expect(getCacheInfoMock).toHaveBeenCalledTimes(1);
68
+ disposeSecond();
69
+
70
+ now += 5001;
71
+
72
+ const disposeThird = mount();
73
+ expect(getCacheInfoMock).toHaveBeenCalledTimes(2);
74
+ disposeThird();
75
+ });
76
+
77
+ it("allows an immediate retry after a failed fetch instead of waiting out the interval", async () => {
78
+ getCacheInfoMock.mockRejectedValueOnce(new Error("kernel not ready"));
79
+
80
+ const disposeFirst = mount();
81
+ expect(getCacheInfoMock).toHaveBeenCalledTimes(1);
82
+ // Flush the microtask queue so the effect's `.catch()` runs and resets
83
+ // the throttle before the next hover.
84
+ await act(async () => {
85
+ await Promise.resolve();
86
+ });
87
+ disposeFirst();
88
+
89
+ const disposeSecond = mount();
90
+ expect(getCacheInfoMock).toHaveBeenCalledTimes(2);
91
+ disposeSecond();
92
+ });
93
+ });
@@ -0,0 +1,33 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import {
4
+ expandAccordionSection,
5
+ fileExplorerPanelAtom,
6
+ sessionPanelAtom,
7
+ } from "@/components/editor/chrome/panels/panel-accordion-state";
8
+ import { openPanel } from "@/components/editor/chrome/state";
9
+ import { assertNever } from "@/utils/assertNever";
10
+ import type { CodeLensKind } from "./entities";
11
+
12
+ /**
13
+ * Opens the panel associated with a code lens target, expanding the accordion
14
+ * section that holds it.
15
+ */
16
+ export function openLensTarget(kind: CodeLensKind): void {
17
+ switch (kind) {
18
+ case "table":
19
+ case "connection":
20
+ openPanel("variables");
21
+ expandAccordionSection(sessionPanelAtom, "datasources");
22
+ return;
23
+ case "bucket":
24
+ openPanel("files");
25
+ expandAccordionSection(fileExplorerPanelAtom, "remote-storage");
26
+ return;
27
+ case "cache":
28
+ openPanel("cache");
29
+ return;
30
+ default:
31
+ assertNever(kind);
32
+ }
33
+ }