@marimo-team/islands 0.23.10-dev5 → 0.23.10-dev50

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 (183) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CyV83R2m.js → ConnectedDataExplorerComponent-Z3RP7Vmm.js} +31 -31
  2. package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
  3. package/dist/{any-language-editor-DfdpyDv_.js → any-language-editor-CAgFD4Kd.js} +20 -20
  4. package/dist/assets/__vite-browser-external-eshhtsgZ.js +1 -0
  5. package/dist/assets/worker-CC0Oul9k.js +73 -0
  6. package/dist/{chat-ui-1rLrACXD.js → chat-ui-BxwraqS-.js} +77 -77
  7. package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
  8. package/dist/{chunk-5FQGJX7Z-BNjes6Yx.js → chunk-5FQGJX7Z-BbqSm5gU.js} +1227 -1222
  9. package/dist/{code-block-37QAKDTI-m92Yc8pv.js → code-block-37QAKDTI-O6IH6wAi.js} +1 -1
  10. package/dist/{code-visibility-Dek9JNl9.js → code-visibility-DKAnHkUo.js} +2166 -1292
  11. package/dist/{copy-BuQpJEzp.js → copy-UqRYxiOg.js} +33 -33
  12. package/dist/dist-7QfXoMdB.js +5 -0
  13. package/dist/{dist-DgnE8F-r.js → dist-A2846XWO.js} +1 -1
  14. package/dist/dist-BEXXyZig.js +5 -0
  15. package/dist/{dist-B3pZ0Ab6.js → dist-BR_gyG9L.js} +3 -3
  16. package/dist/{dist-CcXxepx6.js → dist-BSAt6RhH.js} +27 -27
  17. package/dist/{dist-Bde4a2kU.js → dist-BY018Paw.js} +8 -8
  18. package/dist/dist-BYj57OV4.js +5 -0
  19. package/dist/{dist-CUCNs1ja.js → dist-BaoDKvdy.js} +2 -2
  20. package/dist/{dist-Cy1WxgBD.js → dist-Bf7SHuNp.js} +5 -5
  21. package/dist/{dist-Bz_sYWbr.js → dist-Bk75fBZA.js} +2 -2
  22. package/dist/dist-BlSvQzNr.js +5 -0
  23. package/dist/{dist-C5VC_yzu.js → dist-BzEzfugY.js} +1 -1
  24. package/dist/dist-CCBlxAgS.js +8 -0
  25. package/dist/dist-CIDTVIUf.js +5 -0
  26. package/dist/{dist-CLUtPrdy.js → dist-CIYBwstr.js} +1 -1
  27. package/dist/{dist-BotSqB48.js → dist-C_Y3oV3C.js} +12 -12
  28. package/dist/{dist-BTfv03uy.js → dist-CcWX6tmx.js} +2 -2
  29. package/dist/{dist-BhM8gdSO.js → dist-CoXAujgg.js} +4 -4
  30. package/dist/{dist-4j4c7bjm.js → dist-CpxNdDkw.js} +3 -3
  31. package/dist/dist-CqQyhAM8.js +8 -0
  32. package/dist/dist-CwRu2Xzh.js +5 -0
  33. package/dist/{dist-BcuoonNH.js → dist-CxJDU6Bh.js} +9 -9
  34. package/dist/{dist-DxvORzUR.js → dist-D-W5ny5a.js} +8 -8
  35. package/dist/dist-D8CDTVgf.js +6 -0
  36. package/dist/dist-D8DNB0nO.js +8 -0
  37. package/dist/dist-DL6N_q-A.js +5 -0
  38. package/dist/{dist-BbbIBDiQ.js → dist-DMjWuVs8.js} +1 -1
  39. package/dist/dist-DOFbNV_b.js +8 -0
  40. package/dist/dist-DPrYzMY0.js +6 -0
  41. package/dist/{dist-h2c8sZvT.js → dist-DZORgqKY.js} +1 -1
  42. package/dist/{dist-B3P2fFpz.js → dist-DZo4nSS0.js} +14 -14
  43. package/dist/{dist-D4CewLk6.js → dist-Dax--nl9.js} +1 -1
  44. package/dist/{dist-DRfcqpxJ.js → dist-DgGbNavJ.js} +2 -2
  45. package/dist/{dist-C1BYNeCR.js → dist-Dk6PV_d3.js} +10 -10
  46. package/dist/{dist-fQ0ViXGs.js → dist-Dv_Y15yk.js} +107 -107
  47. package/dist/{dist-Bfwsv11D.js → dist-DyyjKEYf.js} +2 -2
  48. package/dist/{dist-p2qyWijU.js → dist-GZXUmt0b.js} +2 -2
  49. package/dist/{dist-CLJWPTX2.js → dist-LTU8Hdvn.js} +3 -3
  50. package/dist/{dist-DqAWR3CS.js → dist-M9Vag9Y0.js} +20 -20
  51. package/dist/{dist-DNdhYsgW.js → dist-U4F-tbMs.js} +79 -62
  52. package/dist/{dist-RqXTaiir.js → dist-abid3KgM.js} +11 -11
  53. package/dist/dist-cdmMjgsn.js +5 -0
  54. package/dist/dist-hT4QzYX-.js +1247 -0
  55. package/dist/{dist-luvabDEB.js → dist-t9Kf7xqC.js} +2 -2
  56. package/dist/{error-banner-5bz0L9hS.js → error-banner-CJXYJ6Sb.js} +11 -6
  57. package/dist/esm-BaH2eg5-.js +1171 -0
  58. package/dist/{esm-Duie8iU-.js → esm-ga2Bf3O2.js} +43 -43
  59. package/dist/{extends-BgdxCfYu.js → extends-D_hDsj6R.js} +4 -4
  60. package/dist/files/wasm-intro.py +11 -8
  61. package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
  62. package/dist/{glide-data-editor-BOmK9ETQ.js → glide-data-editor-BPkCPs7L.js} +255 -255
  63. package/dist/{html-to-image-CnBdHeLh.js → html-to-image-DXSdAI_Y.js} +2338 -2279
  64. package/dist/{input-_2sjvfne.js → input-OdWHkobi.js} +263 -245
  65. package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
  66. package/dist/main.js +7631 -7559
  67. package/dist/{mermaid-4DMBBIKO-DIdL224_.js → mermaid-4DMBBIKO-Eg3D4FVz.js} +1 -1
  68. package/dist/{mermaid-lXOw5Py9.js → mermaid-BotvIKg9.js} +4 -4
  69. package/dist/{process-output-vFgqkyUT.js → process-output-uozPQhBl.js} +3 -3
  70. package/dist/{reveal-component-C8AmnVfj.js → reveal-component-BxE_9kae.js} +698 -620
  71. package/dist/{spec-B96zNUEA.js → spec-X7FwLJni.js} +4 -4
  72. package/dist/{strings-Bu3vlb6W.js → strings-J57tzLr3.js} +47 -46
  73. package/dist/style.css +1 -1
  74. package/dist/{toDate-x-WRDCH7.js → toDate-d8RCRrRd.js} +2 -2
  75. package/dist/{tooltip-C5FYOpQc.js → tooltip-DpcyNkQ2.js} +2 -2
  76. package/dist/{types-CVvp1fKr.js → types-ChtMFmZ2.js} +1 -1
  77. package/dist/{useAsyncData-iRgKDT5s.js → useAsyncData-PonK__yh.js} +1 -1
  78. package/dist/{useDateFormatter-BA4FCquG.js → useDateFormatter-QB-3MpYr.js} +2 -2
  79. package/dist/{useDeepCompareMemoize-CkQ57VS2.js → useDeepCompareMemoize-D3NGWke6.js} +1 -1
  80. package/dist/{useLifecycle-BBO9PIph.js → useLifecycle-00mO3OSS.js} +2 -2
  81. package/dist/{useTheme-DHIrRQOe.js → useTheme-DEhDzATN.js} +1 -1
  82. package/dist/{vega-component-Dq-SH463.js → vega-component-DGPUhbDs.js} +8 -8
  83. package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
  84. package/package.json +4 -3
  85. package/src/components/chat/chat-display.tsx +2 -2
  86. package/src/components/data-table/TableBottomBar.tsx +1 -15
  87. package/src/components/data-table/TableTopBar.tsx +8 -13
  88. package/src/components/data-table/__tests__/TableBottomBar.test.tsx +6 -12
  89. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +227 -0
  90. package/src/components/data-table/__tests__/data-table.test.tsx +154 -12
  91. package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
  92. package/src/components/data-table/data-table.tsx +1 -1
  93. package/src/components/data-table/filter-by-values-picker.tsx +39 -17
  94. package/src/components/data-table/filter-pills.tsx +1 -1
  95. package/src/components/data-table/hover-tooltip/__tests__/content.test.ts +60 -0
  96. package/src/components/data-table/hover-tooltip/content.ts +44 -0
  97. package/src/components/data-table/hover-tooltip/hover-tooltip.tsx +55 -0
  98. package/src/components/data-table/hover-tooltip/use-table-hover-tooltip.ts +159 -0
  99. package/src/components/data-table/renderers.tsx +27 -43
  100. package/src/components/datasources/__tests__/filter-empty.test.ts +183 -0
  101. package/src/components/datasources/datasources.tsx +92 -3
  102. package/src/components/editor/cell/code/language-toggle.tsx +7 -1
  103. package/src/components/editor/chrome/wrapper/app-chrome.tsx +97 -52
  104. package/src/components/editor/chrome/wrapper/lazy-panels.ts +91 -0
  105. package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -0
  106. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +9 -2
  107. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +62 -27
  108. package/src/components/editor/connections/storage/add-storage-form.tsx +5 -1
  109. package/src/components/editor/connections/storage/as-code.ts +20 -6
  110. package/src/components/editor/documentation.css +35 -0
  111. package/src/components/editor/file-tree/file-explorer.tsx +8 -18
  112. package/src/components/editor/file-tree/tree-actions.tsx +46 -1
  113. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +20 -0
  114. package/src/components/editor/renderers/slides-layout/types.ts +1 -0
  115. package/src/components/markdown/__tests__/markdown-renderer.test.tsx +43 -0
  116. package/src/components/markdown/markdown-renderer.tsx +24 -1
  117. package/src/components/slides/__tests__/minimap-actions.test.tsx +166 -0
  118. package/src/components/slides/__tests__/reveal-component.test.ts +425 -0
  119. package/src/components/slides/minimap.tsx +127 -10
  120. package/src/components/slides/reveal-component.tsx +287 -61
  121. package/src/components/slides/slide-cell-view.tsx +26 -2
  122. package/src/components/slides/slide-form.tsx +26 -4
  123. package/src/components/storage/__tests__/storage-inspector.test.ts +53 -0
  124. package/src/components/storage/storage-inspector.tsx +115 -44
  125. package/src/components/ui/__tests__/use-toast.test.ts +75 -0
  126. package/src/components/ui/combobox.tsx +51 -32
  127. package/src/components/ui/number-field.tsx +5 -1
  128. package/src/components/ui/reorderable-list.tsx +13 -0
  129. package/src/components/ui/select-core/__tests__/use-select-list.test.ts +294 -0
  130. package/src/components/ui/select-core/__tests__/utils.test.ts +222 -0
  131. package/src/components/ui/select-core/index.ts +16 -0
  132. package/src/components/ui/select-core/option-row.tsx +33 -0
  133. package/src/components/ui/select-core/render-slot.ts +20 -0
  134. package/src/components/ui/select-core/select-list.tsx +248 -0
  135. package/src/components/ui/select-core/types.ts +44 -0
  136. package/src/components/ui/select-core/use-select-list.ts +347 -0
  137. package/src/components/ui/select-core/utils.ts +121 -0
  138. package/src/components/ui/use-toast.ts +33 -13
  139. package/src/core/codemirror/language/languages/python.ts +2 -0
  140. package/src/core/codemirror/language/languages/sql/utils.ts +3 -1
  141. package/src/core/codemirror/lsp/__tests__/markdown-renderer.test.ts +41 -0
  142. package/src/core/codemirror/lsp/markdown-renderer.ts +59 -0
  143. package/src/core/datasets/data-source-connections.ts +2 -0
  144. package/src/core/network/__tests__/requests-static.test.ts +30 -0
  145. package/src/core/network/requests-static.ts +14 -10
  146. package/src/core/storage/__tests__/state.test.ts +49 -0
  147. package/src/core/storage/__tests__/useStorageEntries.test.tsx +199 -3
  148. package/src/core/storage/state.ts +124 -13
  149. package/src/core/storage/types.ts +6 -0
  150. package/src/core/wasm/worker/bootstrap.ts +12 -4
  151. package/src/plugins/core/__test__/registerReactComponent.test.ts +96 -0
  152. package/src/plugins/core/registerReactComponent.tsx +42 -10
  153. package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
  154. package/src/plugins/impl/SearchableSelect.tsx +16 -97
  155. package/src/plugins/impl/SliderPlugin.tsx +132 -18
  156. package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
  157. package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
  158. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +278 -8
  159. package/src/plugins/layout/DownloadPlugin.tsx +1 -1
  160. package/src/utils/__tests__/numbers.test.ts +20 -0
  161. package/src/utils/errors.ts +9 -0
  162. package/src/utils/lazy.ts +6 -1
  163. package/src/utils/numbers.ts +27 -0
  164. package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +0 -1
  165. package/dist/assets/worker-ip3AI_sN.js +0 -73
  166. package/dist/dist-0Fif7jnk.js +0 -5
  167. package/dist/dist-B5h_9sHB.js +0 -6
  168. package/dist/dist-B9M6R5ye.js +0 -5
  169. package/dist/dist-BCt3tnck.js +0 -8
  170. package/dist/dist-BUIJwMwn.js +0 -8
  171. package/dist/dist-BpquMd3k.js +0 -5
  172. package/dist/dist-BzJsqYfz.js +0 -5
  173. package/dist/dist-CA5ELXAf.js +0 -6
  174. package/dist/dist-CLBRs6Uv.js +0 -5
  175. package/dist/dist-CStVCMbq.js +0 -5
  176. package/dist/dist-CZRIEY3Y.js +0 -8
  177. package/dist/dist-CuUHbFD0.js +0 -5
  178. package/dist/dist-DV7Iabxb.js +0 -8
  179. package/dist/dist-DhHh0jLg.js +0 -1247
  180. package/dist/dist-DuEeHMvL.js +0 -5
  181. package/dist/esm-BfhQmZjp.js +0 -1171
  182. package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
  183. /package/src/components/{data-table → ui}/value-chips.tsx +0 -0
@@ -0,0 +1,121 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { BulkActionSpec, Option } from "./types";
3
+
4
+ /**
5
+ * Override cmdk's relevance-sorting filter with a stricter membership test: an
6
+ * option matches only when every whitespace-separated query word appears in it
7
+ * (case-insensitive), in any order. More lenient than exact match, stricter than
8
+ * fuzzy.
9
+ *
10
+ * Examples:
11
+ * - "foo bar" matches "foo bar"
12
+ * - "bar foo" matches "foo bar"
13
+ * - "foob" does not match "foo bar"
14
+ */
15
+ export function multiselectFilterFn(option: string, value: string): number {
16
+ const words = value.split(/\s+/);
17
+ const match = words.every((word) =>
18
+ option.toLowerCase().includes(word.toLowerCase()),
19
+ );
20
+ return match ? 1 : 0;
21
+ }
22
+
23
+ /** Union: append the not-yet-selected members of `toAdd`, preserving order. */
24
+ export function selectMatching<V>(selected: V[], toAdd: V[]): V[] {
25
+ const set = new Set(selected);
26
+ return [...set, ...toAdd.filter((v) => !set.has(v))];
27
+ }
28
+
29
+ /** Difference: drop every member of `toRemove` from `selected`. */
30
+ export function deselectMatching<V>(selected: V[], toRemove: V[]): V[] {
31
+ const set = new Set(toRemove);
32
+ return selected.filter((v) => !set.has(v));
33
+ }
34
+
35
+ /**
36
+ * Order options for the idle (no active search) menu: pinned selections first in
37
+ * pin insertion order (so a freshly added selection appears after earlier ones),
38
+ * then everything else in option order. `pinnedSelection` is a frozen snapshot
39
+ * taken when the menu opens, so toggling an item does not reorder the list under
40
+ * the cursor. Returns the count of pinned options alongside the list so callers
41
+ * don't have to re-scan to find where the pinned block ends.
42
+ */
43
+ export function getVisibleOptions<V>(
44
+ options: Array<Option<V>>,
45
+ pinnedSelection: ReadonlySet<V>,
46
+ ): { visibleOptions: Array<Option<V>>; pinnedCount: number } {
47
+ const byValue = new Map(options.map((o) => [o.value, o] as const));
48
+ const pinned: Array<Option<V>> = [];
49
+ for (const value of pinnedSelection) {
50
+ const option = byValue.get(value);
51
+ if (option) {
52
+ pinned.push(option);
53
+ }
54
+ }
55
+ const rest = options.filter((o) => !pinnedSelection.has(o.value));
56
+ return { visibleOptions: [...pinned, ...rest], pinnedCount: pinned.length };
57
+ }
58
+
59
+ /**
60
+ * Decide which bulk rows the menu shows for the current search/cap state.
61
+ *
62
+ * When searching, the select-side spec carries the unselected matches and the
63
+ * deselect-side spec carries the selected matches (each omitted when empty).
64
+ * When idle, the spec is just `select-all` / `deselect-all` with an `enabled`
65
+ * flag for the disabled-but-visible state. `maxSelections` hides the select-
66
+ * side everywhere (a bulk select could exceed the cap); `maxSelections === 1`
67
+ * suppresses bulk rows entirely. Returns `[]` when bulk rows shouldn't show.
68
+ *
69
+ * Specs come in select-then-deselect order so the facade can render them as-is.
70
+ */
71
+ export function getBulkActions<V>(params: {
72
+ options: Array<Option<V>>;
73
+ filteredOptions: Array<Option<V>>;
74
+ value: V[];
75
+ searchQuery: string;
76
+ maxSelections: number | undefined;
77
+ }): Array<BulkActionSpec<V>> {
78
+ const { options, filteredOptions, value, searchQuery, maxSelections } =
79
+ params;
80
+
81
+ if (options.length <= 2 || maxSelections === 1) {
82
+ return [];
83
+ }
84
+
85
+ const capped = maxSelections != null;
86
+ const specs: Array<BulkActionSpec<V>> = [];
87
+
88
+ if (searchQuery !== "") {
89
+ if (filteredOptions.length === 0) {
90
+ return [];
91
+ }
92
+ const selected = new Set(value);
93
+ const selectedMatches: Array<Option<V>> = [];
94
+ const unselectedMatches: Array<Option<V>> = [];
95
+ for (const option of filteredOptions) {
96
+ if (selected.has(option.value)) {
97
+ selectedMatches.push(option);
98
+ } else if (!option.disabled) {
99
+ // Disabled rows can't be picked individually, so bulk select skips them too.
100
+ unselectedMatches.push(option);
101
+ }
102
+ }
103
+ if (!capped && unselectedMatches.length > 0) {
104
+ specs.push({ kind: "select-matching", items: unselectedMatches });
105
+ }
106
+ if (selectedMatches.length > 0) {
107
+ specs.push({ kind: "deselect-matching", items: selectedMatches });
108
+ }
109
+ return specs;
110
+ }
111
+
112
+ if (!capped) {
113
+ const selected = new Set(value);
114
+ specs.push({
115
+ kind: "select-all",
116
+ enabled: options.some((o) => !o.disabled && !selected.has(o.value)),
117
+ });
118
+ }
119
+ specs.push({ kind: "deselect-all", enabled: value.length > 0 });
120
+ return specs;
121
+ }
@@ -58,6 +58,10 @@ interface State {
58
58
 
59
59
  const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
60
60
 
61
+ // Keys of toasts requested with `once: true`, suppressed after first show
62
+ // for the lifetime of the page (i.e. one static-preview session).
63
+ const shownOnceKeys = new Set<string>();
64
+
61
65
  const addToRemoveQueue = (toastId: string) => {
62
66
  if (toastTimeouts.has(toastId)) {
63
67
  return;
@@ -159,9 +163,18 @@ function dispatch(action: Action) {
159
163
 
160
164
  type Toast = Omit<ToasterToast, "id">;
161
165
 
162
- function toast({ id, ...props }: Toast & { id?: string }) {
166
+ function toast({
167
+ id,
168
+ once,
169
+ ...props
170
+ }: Toast & { id?: string; once?: boolean }) {
163
171
  const toastId = id || genId();
164
172
 
173
+ // `once` dedupe requires a caller-provided stable `id`. A generated id is
174
+ // unique per call, so it could never match (and would grow the set without
175
+ // bound); gate on `id` so `once` without one is simply a no-op.
176
+ const dedupeOnce = once === true && id !== undefined;
177
+
165
178
  const update = (props: Toast) =>
166
179
  dispatch({
167
180
  type: "UPDATE_TOAST",
@@ -183,19 +196,26 @@ function toast({ id, ...props }: Toast & { id?: string }) {
183
196
  },
184
197
  });
185
198
 
186
- dispatch({
187
- type: "ADD_TOAST",
188
- toast: {
189
- ...props,
190
- id: toastId,
191
- open: true,
192
- onOpenChange: (open) => {
193
- if (!open) {
194
- dismiss();
195
- }
199
+ const suppressed = dedupeOnce && shownOnceKeys.has(toastId);
200
+ if (dedupeOnce) {
201
+ shownOnceKeys.add(toastId);
202
+ }
203
+
204
+ if (!suppressed) {
205
+ dispatch({
206
+ type: "ADD_TOAST",
207
+ toast: {
208
+ ...props,
209
+ id: toastId,
210
+ open: true,
211
+ onOpenChange: (open) => {
212
+ if (!open) {
213
+ dismiss();
214
+ }
215
+ },
196
216
  },
197
- },
198
- });
217
+ });
218
+ }
199
219
 
200
220
  return {
201
221
  id: toastId,
@@ -33,6 +33,7 @@ import { cellActionsState } from "../../cells/state";
33
33
  import { pythonCompletionSource } from "../../completion/completer";
34
34
  import type { PlaceholderType } from "../../config/types";
35
35
  import { FederatedLanguageServerClient } from "../../lsp/federated-lsp";
36
+ import { createLspMarkdownRenderer } from "../../lsp/markdown-renderer";
36
37
  import { NotebookLanguageServerClient } from "../../lsp/notebook-lsp";
37
38
  import { createTransport } from "../../lsp/transports";
38
39
  import { CellDocumentUri, type ILanguageServerClient } from "../../lsp/types";
@@ -342,6 +343,7 @@ export class PythonLanguageAdapter implements LanguageAdapter<{}> {
342
343
  client: client as unknown as LanguageServerClient,
343
344
  languageId: "python",
344
345
  allowHTMLContent: true,
346
+ markdownRenderer: createLspMarkdownRenderer(),
345
347
  useSnippetOnCompletion: true,
346
348
  hoverConfig: hoverOptions,
347
349
  completionConfig: autocompleteOptions,
@@ -14,6 +14,7 @@ import {
14
14
  } from "@codemirror/lang-sql";
15
15
  import {
16
16
  BigQueryDialect,
17
+ DremioDialect,
17
18
  DuckDBDialect,
18
19
  } from "@marimo-team/codemirror-sql/dialects";
19
20
  import type { DataSourceConnection } from "@/core/kernel/messages";
@@ -97,6 +98,8 @@ export function guessDialect(
97
98
  return PLSQL;
98
99
  case "bigquery":
99
100
  return BigQueryDialect;
101
+ case "dremio":
102
+ return DremioDialect;
100
103
  case "timescaledb":
101
104
  return PostgreSQL; // TimescaleDB is a PostgreSQL dialect
102
105
  case "awsathena":
@@ -116,7 +119,6 @@ export function guessDialect(
116
119
  case "spark":
117
120
  case "databricks":
118
121
  case "datafusion":
119
- case "dremio":
120
122
  Logger.debug("Unsupported dialect", { dialect });
121
123
  return ModifiedStandardSQL;
122
124
  default:
@@ -0,0 +1,41 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { describe, expect, it } from "vitest";
3
+ import { createLspMarkdownRenderer } from "../markdown-renderer";
4
+
5
+ describe("createLspMarkdownRenderer", () => {
6
+ it("syntax-highlights python code blocks with tok-* span classes", () => {
7
+ const render = createLspMarkdownRenderer();
8
+ const result = render("```python\ndef foo():\n pass\n```");
9
+ expect(result).toContain('class="language-python"');
10
+ expect(result).toContain("tok-keyword");
11
+ expect(result).toContain("<pre><code");
12
+ });
13
+
14
+ it("does not highlight non-python code blocks", () => {
15
+ const render = createLspMarkdownRenderer();
16
+ const result = render("```bash\necho hello\n```");
17
+ expect(result).not.toContain("tok-keyword");
18
+ expect(result).toContain("<code");
19
+ });
20
+
21
+ it("renders empty python code blocks as empty string", () => {
22
+ const render = createLspMarkdownRenderer();
23
+ const result = render("```python\n \n```");
24
+ expect(result).toBe("");
25
+ });
26
+
27
+ it("renders markdown prose unchanged", () => {
28
+ const render = createLspMarkdownRenderer();
29
+ const result = render("## Examples\n\nSome text.");
30
+ expect(result).toContain("<h2");
31
+ expect(result).toContain("Examples");
32
+ expect(result).toContain("Some text.");
33
+ });
34
+
35
+ it("escapes HTML in highlighted code", () => {
36
+ const render = createLspMarkdownRenderer();
37
+ const result = render('```python\nx = "<div>"\n```');
38
+ expect(result).not.toContain("<div>");
39
+ expect(result).toContain("&lt;div&gt;");
40
+ });
41
+ });
@@ -0,0 +1,59 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { classHighlighter, highlightCode } from "@lezer/highlight";
3
+ import { parser as pythonParser } from "@lezer/python";
4
+ import { marked } from "marked";
5
+
6
+ /**
7
+ * Syntax-highlight a Python code string using the lezer Python parser.
8
+ * Returns an HTML string with tok-* span classes for styling.
9
+ */
10
+ function highlightPython(code: string): string {
11
+ const tree = pythonParser.parse(code);
12
+ let html = "";
13
+ highlightCode(
14
+ code,
15
+ tree,
16
+ classHighlighter,
17
+ (text, classes) => {
18
+ const escaped = text
19
+ .replace(/&/g, "&amp;")
20
+ .replace(/</g, "&lt;")
21
+ .replace(/>/g, "&gt;");
22
+ html += classes ? `<span class="${classes}">${escaped}</span>` : escaped;
23
+ },
24
+ () => {
25
+ html += "\n";
26
+ },
27
+ );
28
+ return html;
29
+ }
30
+
31
+ /**
32
+ * A markdown renderer for LSP hover tooltips that adds syntax highlighting
33
+ * to Python code blocks using the lezer Python parser and classHighlighter.
34
+ * The tok-* CSS classes are defined in documentation.css.
35
+ */
36
+ export function createLspMarkdownRenderer(): (markdown: string) => string {
37
+ const renderer = new marked.Renderer();
38
+ const prevCode = renderer.code.bind(renderer);
39
+
40
+ renderer.code = (token) => {
41
+ const { text, lang } = token;
42
+ if (!text.trim()) {
43
+ return "";
44
+ }
45
+ if (lang === "python" || lang === "py") {
46
+ const highlighted = highlightPython(text);
47
+ return `<pre><code class="language-python">${highlighted}</code></pre>\n`;
48
+ }
49
+ return prevCode(token);
50
+ };
51
+
52
+ return (markdown: string): string =>
53
+ marked(markdown, {
54
+ async: false,
55
+ gfm: true,
56
+ breaks: true,
57
+ renderer,
58
+ }) as string;
59
+ }
@@ -169,6 +169,7 @@ const {
169
169
  return {
170
170
  ...db,
171
171
  schemas: schemas,
172
+ schemas_resolved: true,
172
173
  };
173
174
  }),
174
175
  };
@@ -213,6 +214,7 @@ const {
213
214
  return {
214
215
  ...schema,
215
216
  tables: tables,
217
+ tables_resolved: true,
216
218
  };
217
219
  }),
218
220
  };
@@ -0,0 +1,30 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { toast } from "@/components/ui/use-toast";
4
+ import { createStaticRequests } from "../requests-static";
5
+
6
+ vi.mock("@/components/ui/use-toast", () => ({ toast: vi.fn() }));
7
+
8
+ describe("createStaticRequests static-notebook toast", () => {
9
+ beforeEach(() => {
10
+ vi.mocked(toast).mockClear();
11
+ });
12
+
13
+ it("requests a once-toast with a stable id on component value updates", async () => {
14
+ const requests = createStaticRequests();
15
+ await requests.sendComponentValues({} as never);
16
+
17
+ expect(toast).toHaveBeenCalledWith(
18
+ expect.objectContaining({ id: "static-notebook", once: true }),
19
+ );
20
+ });
21
+
22
+ it("uses the same once-toast for function requests", async () => {
23
+ const requests = createStaticRequests();
24
+ await requests.sendFunctionRequest({} as never);
25
+
26
+ expect(toast).toHaveBeenCalledWith(
27
+ expect.objectContaining({ id: "static-notebook", once: true }),
28
+ );
29
+ });
30
+ });
@@ -3,6 +3,18 @@ import { toast } from "@/components/ui/use-toast";
3
3
  import { Logger } from "@/utils/Logger";
4
4
  import type { EditRequests, RunRequests } from "./types";
5
5
 
6
+ const STATIC_NOTEBOOK_TOAST_ID = "static-notebook";
7
+
8
+ function showStaticNotebookToast() {
9
+ toast({
10
+ id: STATIC_NOTEBOOK_TOAST_ID,
11
+ once: true,
12
+ title: "Static notebook",
13
+ description:
14
+ "This notebook is not connected to a kernel. Any interactive elements will not work.",
15
+ });
16
+ }
17
+
6
18
  export function createStaticRequests(): EditRequests & RunRequests {
7
19
  const throwNotInEditMode = () => {
8
20
  throw new Error("Unreachable. Expected to be in run mode");
@@ -10,11 +22,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
10
22
 
11
23
  return {
12
24
  sendComponentValues: async () => {
13
- toast({
14
- title: "Static notebook",
15
- description:
16
- "This notebook is not connected to a kernel. Any interactive elements will not work.",
17
- });
25
+ showStaticNotebookToast();
18
26
  Logger.log("Updating UI elements is not supported in static mode");
19
27
  return null;
20
28
  },
@@ -27,11 +35,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
27
35
  return null;
28
36
  },
29
37
  sendFunctionRequest: async () => {
30
- toast({
31
- title: "Static notebook",
32
- description:
33
- "This notebook is not connected to a kernel. Any interactive elements will not work.",
34
- });
38
+ showStaticNotebookToast();
35
39
  Logger.log("Function requests are not supported in static mode");
36
40
  return null;
37
41
  },
@@ -48,6 +48,7 @@ describe("storage state", () => {
48
48
  expect(state).toEqual({
49
49
  namespaces: [],
50
50
  entriesByPath: new Map(),
51
+ pageMetadataByPath: new Map(),
51
52
  });
52
53
  });
53
54
  });
@@ -134,6 +135,49 @@ describe("storage state", () => {
134
135
  expect(state.entriesByPath.get("my_s3::data/")).toEqual(entries);
135
136
  });
136
137
 
138
+ it("should store next page tokens keyed by namespace and prefix", () => {
139
+ const entries = [makeEntry({ path: "a.txt" })];
140
+
141
+ actions.setEntries({
142
+ namespace: "my_s3",
143
+ prefix: "data/",
144
+ entries,
145
+ nextPageToken: "150",
146
+ });
147
+
148
+ expect(state.entriesByPath.get("my_s3::data/")).toEqual(entries);
149
+ expect(state.pageMetadataByPath.get("my_s3::data/")?.nextPageToken).toBe(
150
+ "150",
151
+ );
152
+ });
153
+
154
+ it("should append entries when requested", () => {
155
+ const firstPage = [makeEntry({ path: "a.txt" })];
156
+ const secondPage = [makeEntry({ path: "b.txt" })];
157
+
158
+ actions.setEntries({
159
+ namespace: "my_s3",
160
+ prefix: "data/",
161
+ entries: firstPage,
162
+ nextPageToken: "150",
163
+ });
164
+ actions.setEntries({
165
+ namespace: "my_s3",
166
+ prefix: "data/",
167
+ entries: secondPage,
168
+ nextPageToken: null,
169
+ append: true,
170
+ });
171
+
172
+ expect(state.entriesByPath.get("my_s3::data/")).toEqual([
173
+ ...firstPage,
174
+ ...secondPage,
175
+ ]);
176
+ expect(
177
+ state.pageMetadataByPath.get("my_s3::data/")?.nextPageToken,
178
+ ).toBeNull();
179
+ });
180
+
137
181
  it("should use empty string for null prefix", () => {
138
182
  const entries = [makeEntry({ path: "root.txt" })];
139
183
 
@@ -251,6 +295,11 @@ describe("storage state", () => {
251
295
  expect(state.entriesByPath.get("my_s3::")).toBeUndefined();
252
296
  expect(state.entriesByPath.get("my_s3::data/")).toBeUndefined();
253
297
  expect(state.entriesByPath.get("my_s3::data/nested/")).toBeUndefined();
298
+ expect(state.pageMetadataByPath.get("my_s3::")).toBeUndefined();
299
+ expect(state.pageMetadataByPath.get("my_s3::data/")).toBeUndefined();
300
+ expect(
301
+ state.pageMetadataByPath.get("my_s3::data/nested/"),
302
+ ).toBeUndefined();
254
303
  });
255
304
 
256
305
  it("should not affect entries from other namespaces", () => {