@marimo-team/frontend 0.23.16-dev3 → 0.23.16-dev31
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.
- package/dist/assets/{CellStatus-BalYv0pK.js → CellStatus-CmojVYzm.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-DI7mV-Gw.js → ConnectedDataExplorerComponent-CMaZapRd.js} +1 -1
- package/dist/assets/{ErrorBoundary-C7_5jjWi.js → ErrorBoundary-DzTD8LVq.js} +1 -1
- package/dist/assets/{ImperativeModal-BIavOiUv.js → ImperativeModal-fY-ZfT0b.js} +1 -1
- package/dist/assets/{JsonOutput-CkCD-FgM.js → JsonOutput-BwwRpByA.js} +12 -12
- package/dist/assets/{LazyAnyLanguageCodeMirror-sVm_hz8A.js → LazyAnyLanguageCodeMirror-B2JvSXP-.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-DO2k2fA5.js → MarimoErrorOutput-BWte8vjk.js} +4 -4
- package/dist/assets/{RSPContexts-CgC81f0I.js → RSPContexts-WiCgGm__.js} +1 -1
- package/dist/assets/RenderHTML-DRltV71H.js +1 -0
- package/dist/assets/{RunButton-BKRupqr7.js → RunButton-DIegd2mN.js} +1 -1
- package/dist/assets/{add-cell-with-ai-CEZtaZCK.js → add-cell-with-ai-BBFD-M9V.js} +17 -17
- package/dist/assets/add-connection-dialog-CECJ8507.js +170 -0
- package/dist/assets/{agent-panel-CHB6t7Zy.js → agent-panel-YaS0WXP1.js} +5 -5
- package/dist/assets/ai-model-dropdown-BpVBUZnz.js +5 -0
- package/dist/assets/{alert-dialog-BXegNC0I.js → alert-dialog-Bb8Lz8ko.js} +1 -1
- package/dist/assets/{any-language-editor-DCpZRrpx.js → any-language-editor--tRHs8C_.js} +1 -1
- package/dist/assets/{app-config-button-NrM9ywXA.js → app-config-button-BW-w01aF.js} +1 -1
- package/dist/assets/{button-eIihcbUw.js → button-DDGiD1NW.js} +1 -1
- package/dist/assets/{cache-panel-wllSTlbM.js → cache-panel-DcQUeUpg.js} +1 -1
- package/dist/assets/{capabilities-B09j1Eor.js → capabilities-DoEg0XwL.js} +1 -1
- package/dist/assets/{cell-editor-B5JVq7_q.js → cell-editor-EV-1IccQ.js} +10 -10
- package/dist/assets/cell-link-C5gpAn3R.js +1 -0
- package/dist/assets/cells-6yd8nOAu.js +231 -0
- package/dist/assets/{chat-display-BG08ltQ3.js → chat-display-8BbC5B-D.js} +1 -1
- package/dist/assets/{chat-panel-P2CzCtXJ.js → chat-panel-00V3uFDx.js} +3 -3
- package/dist/assets/{chat-ui-fAJZAoTD.js → chat-ui-DWhJ2Ozy.js} +1 -1
- package/dist/assets/{column-preview-Dne1dIt0.js → column-preview-DeXESYpM.js} +1 -1
- package/dist/assets/{command-CAJLPEBq.js → command-Dqxie4gq.js} +1 -1
- package/dist/assets/{command-palette-iOCqUIs1.js → command-palette-DZ2rNzzf.js} +1 -1
- package/dist/assets/{common-CD6Mj8n6.js → common-XVELED_O.js} +1 -1
- package/dist/assets/components-BLHpmhXx.js +1 -0
- package/dist/assets/config-B-Q7J_6d.js +1 -0
- package/dist/assets/{config-COUOIPEq.js → config-BJVx3eBW.js} +1 -1
- package/dist/assets/{copy-BtTOqPTS.js → copy-Bxb1_Gtw.js} +1 -1
- package/dist/assets/{copy-icon-C2pG0ZNK.js → copy-icon-neDQUo4Q.js} +1 -1
- package/dist/assets/{createReducer-C7kZ8zEe.js → createReducer-DGx5Af-C.js} +1 -1
- package/dist/assets/{datasource-ySAiC9gr.js → datasource-CTqa8bf-.js} +2 -2
- package/dist/assets/{dates-D-kIKDYA.js → dates-C2LWzsCZ.js} +1 -1
- package/dist/assets/{dependency-graph-panel-BF23EwcD.js → dependency-graph-panel-C4Nzcddp.js} +4 -4
- package/dist/assets/{dialog-D1DEsRZ2.js → dialog-BCVepWLJ.js} +1 -1
- package/dist/assets/{dist-ByVeMWkr.js → dist-BLN8PNJV.js} +1 -1
- package/dist/assets/dist-CiVShZKM.js +2 -0
- package/dist/assets/{documentation-panel-e9Fi9R7e.js → documentation-panel-Bym4G7IS.js} +1 -1
- package/dist/assets/download-HKgqJX4l.js +9 -0
- package/dist/assets/{dropdown-menu-BFZLnWeO.js → dropdown-menu-dHmZR8bI.js} +1 -1
- package/dist/assets/{edit-page-DSEkwJ4O.js → edit-page-D80ascX2.js} +6 -6
- package/dist/assets/{error-banner-B6eIlxzg.js → error-banner-BSxEOIn2.js} +1 -1
- package/dist/assets/{error-entries-Birw-8VX.js → error-entries-CxyvaxEM.js} +2 -2
- package/dist/assets/{error-panel-DdsUL6b7.js → error-panel-CfSy7uUr.js} +1 -1
- package/dist/assets/{es-1D83cgiu.js → es-gYgfF8y5.js} +1 -1
- package/dist/assets/{esm-BL7jQmzn.js → esm-B30N1GS0.js} +1 -1
- package/dist/assets/{field-BeZbVzXA.js → field-BsxD35VE.js} +1 -1
- package/dist/assets/file-explorer-panel-Dd5Xn2TK.js +26 -0
- package/dist/assets/{file-icons-Cb54mIL8.js → file-icons-CSjJ3JC6.js} +1 -1
- package/dist/assets/{file-name-input-BWFcK1ey.js → file-name-input-CrKtrXMl.js} +1 -1
- package/dist/assets/floating-outline-ByQ8vvif.js +1 -0
- package/dist/assets/focus-CVONl-Dy.js +1 -0
- package/dist/assets/{form-B6BkSCJb.js → form-Ca3yd_Od.js} +1 -1
- package/dist/assets/{formats-ClEFGPy-.js → formats-DzmLpc9A.js} +1 -1
- package/dist/assets/{formatting-B_41gSF5.js → formatting-CDf6w1Kt.js} +1 -1
- package/dist/assets/{fullscreen-dUjPYOmm.js → fullscreen-CTDqNQr6.js} +1 -1
- package/dist/assets/{gallery-page-CWQQAYpa.js → gallery-page-CJ3ms1Hn.js} +1 -1
- package/dist/assets/{glide-data-editor-lMARFWHS.js → glide-data-editor-j2igs9Y_.js} +1 -1
- package/dist/assets/home-page-FpAawj9s.js +4 -0
- package/dist/assets/{html-to-image-CwmX28T9.js → html-to-image-C-4ysbGe.js} +2 -2
- package/dist/assets/index-4k5eCUzO.js +38 -0
- package/dist/assets/index-xs-zSSrX.css +2 -0
- package/dist/assets/{input-CqrUjynb.js → input-JxFwu-3G.js} +1 -1
- package/dist/assets/kiosk-mode-BumOFEtY.js +1 -0
- package/dist/assets/{label-BElwWzf0.js → label-CqjrpwRC.js} +1 -1
- package/dist/assets/{layout-CUlZijwG.js → layout-Dtql_o_T.js} +5 -5
- package/dist/assets/{links-CToBjIIj.js → links-CZr5bjvo.js} +1 -1
- package/dist/assets/{logs-panel-iFV4ZJ9b.js → logs-panel-5u1wUBn1.js} +1 -1
- package/dist/assets/{markdown-renderer-QvXG0CRk.js → markdown-renderer-dABDqZDw.js} +2 -2
- package/dist/assets/{menu-items-DbSLBfXR.js → menu-items-CCi6aOPm.js} +1 -1
- package/dist/assets/{mermaid-Bh0flEIb.js → mermaid-CYN0wOyG.js} +1 -1
- package/dist/assets/name-cell-input-Y4knmEIU.js +1 -0
- package/dist/assets/node-sql-parser-Si4oZ2P-.js +68 -0
- package/dist/assets/{numbers-CeeaFKqu.js → numbers-rmawRqQP.js} +1 -1
- package/dist/assets/outline-panel-CQ_KWLip.js +1 -0
- package/dist/assets/{packages-panel-BzQF60rG.js → packages-panel-Ijt7d51P.js} +1 -1
- package/dist/assets/{pair-with-agent-modal-CKOYwOYw.js → pair-with-agent-modal-BUtnUuiE.js} +1 -1
- package/dist/assets/panels-DfU1Oikj.js +4 -0
- package/dist/assets/{popover-DrHQ3KEt.js → popover-Bcz1Zikb.js} +1 -1
- package/dist/assets/process-output-Dd11YI04.js +1 -0
- package/dist/assets/{radio-group-CLBjGkt5.js → radio-group-22eWJ9fv.js} +1 -1
- package/dist/assets/readonly-code-display-BjK-TEri.js +1 -0
- package/dist/assets/{readonly-python-code-CW_G5e4E.js → readonly-python-code-n_TrLmsR.js} +1 -1
- package/dist/assets/{renderShortcut-BZLr8D7H.js → renderShortcut-Bl8_FVn8.js} +1 -1
- package/dist/assets/{reveal-component-BLMTmd8T.js → reveal-component-CCLTV1q6.js} +60 -60
- package/dist/assets/{run-page-B6pgDYEJ.js → run-page-DbezK7Qs.js} +1 -1
- package/dist/assets/{runs-D51Xwrsk.js → runs-DBTXqJHS.js} +1 -1
- package/dist/assets/scratchpad-panel-C2wAEv5n.js +1 -0
- package/dist/assets/{scroll-to-bottom-button-Cgr10kaJ.js → scroll-to-bottom-button-C8yfT4wq.js} +1 -1
- package/dist/assets/{secrets-panel-Cc5KDBk6.js → secrets-panel-6J6wcuVR.js} +1 -1
- package/dist/assets/{select-B5TFNjmU.js → select-BP75_5jJ.js} +1 -1
- package/dist/assets/{session-CEFnN9Cq.js → session-BmukRjSi.js} +1 -1
- package/dist/assets/{session-panel-Y0Oon0sD.js → session-panel-BZGNcZmp.js} +1 -1
- package/dist/assets/{share-CYpcX4N4.js → share-852X_cqy.js} +1 -1
- package/dist/assets/{skeleton-D3QPxqWC.js → skeleton-BzlKcq21.js} +1 -1
- package/dist/assets/slides-DkEgzTJB.css +1 -0
- package/dist/assets/{snippets-panel-CXpGflMO.js → snippets-panel-DNaNtkR0.js} +1 -1
- package/dist/assets/{spec-CzqcmYQk.js → spec-C7opZZRp.js} +1 -1
- package/dist/assets/{state-BvdK8eiw.js → state-D10U2MwA.js} +1 -1
- package/dist/assets/{state-WBgc7MiA.js → state-DDag0B_B.js} +1 -1
- package/dist/assets/state-DFPAfLw7.js +3 -0
- package/dist/assets/{state-DBzEQtWz.js → state-jCdh8Dcx.js} +1 -1
- package/dist/assets/{strings-Djnbwmfl.js → strings--BknHX9C.js} +1 -1
- package/dist/assets/{swiper-component-DInvCxhI.js → swiper-component-g8EXE911.js} +1 -1
- package/dist/assets/switch-OVajJBPR.js +1 -0
- package/dist/assets/{terminal-D3IQBBYo.js → terminal-217O8hlZ.js} +1 -1
- package/dist/assets/{textarea-CpFfEUet.js → textarea-DuEtSOCl.js} +1 -1
- package/dist/assets/{tooltip-CFX4fYHU.js → tooltip-4bZlU7jl.js} +1 -1
- package/dist/assets/{tooltip-HLb6RxVp.js → tooltip-B7U4xkIN.js} +1 -1
- package/dist/assets/{tracing-panel-BU-o_LCd.js → tracing-panel-rVAa3En0.js} +2 -2
- package/dist/assets/{tracing-CKn3gWHw.js → tracing-ttUd3J7U.js} +1 -1
- package/dist/assets/{tree-actions-CsC2XmGo.js → tree-actions-UCr1tMHw.js} +1 -1
- package/dist/assets/{useBoolean-D89u30n-.js → useBoolean-CFj86iac.js} +1 -1
- package/dist/assets/useCellActionButton-CJknR0XY.js +1 -0
- package/dist/assets/{useDeleteCell-EPH4kcG7.js → useDeleteCell-PrgejW8A.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-CZnK7N2L.js → useDependencyPanelTab-5jc-_-hy.js} +1 -1
- package/dist/assets/useEventListener-C0195JsF.js +1 -0
- package/dist/assets/{useHotkey-DIuRH8Wu.js → useHotkey-DLZvrRZT.js} +1 -1
- package/dist/assets/{useIframeCapabilities-Ccb4fvKc.js → useIframeCapabilities-j6fR_dBA.js} +1 -1
- package/dist/assets/{useInstallPackage-BRbUAUFn.js → useInstallPackage-WiYBwhUO.js} +1 -1
- package/dist/assets/{useInterval-CC0vKpWm.js → useInterval-CWBTUy5W.js} +1 -1
- package/dist/assets/useNotebookActions-DG8f4m8p.js +12 -0
- package/dist/assets/useRunCells-DHsd7HGL.js +1 -0
- package/dist/assets/{useSplitCell-9Qz93yPZ.js → useSplitCell-MZpER0Bh.js} +1 -1
- package/dist/assets/{useTheme-KXqAyDPC.js → useTheme-Bxv9B31g.js} +1 -1
- package/dist/assets/{utils-Z80SV4_d.js → utils-BWBBpCkT.js} +1 -1
- package/dist/assets/{vega-component-Cie3ag_T.js → vega-component-Db8KjZJG.js} +1 -1
- package/dist/assets/{write-secret-modal-DEEn7sZV.js → write-secret-modal-3-v-dHVj.js} +1 -1
- package/dist/index.html +68 -68
- package/package.json +2 -2
- package/src/__mocks__/requests.ts +45 -4
- package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
- package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
- package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
- package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
- package/src/components/data-table/column-summary/utils.ts +26 -7
- package/src/components/data-table/types.ts +1 -1
- package/src/components/editor/actions/__tests__/pdf-export.test.ts +0 -4
- package/src/components/editor/actions/pdf-export.ts +1 -4
- package/src/components/editor/actions/useNotebookActions.tsx +7 -13
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +50 -0
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +87 -8
- package/src/components/editor/connections/storage/as-code.ts +40 -21
- package/src/components/editor/connections/storage/schemas.ts +83 -42
- package/src/components/editor/file-tree/__tests__/file-render-mode.test.ts +27 -0
- package/src/components/editor/file-tree/__tests__/file-viewer.test.tsx +150 -0
- package/src/components/editor/file-tree/file-preview-metadata.tsx +51 -0
- package/src/components/editor/file-tree/file-render-mode.ts +32 -0
- package/src/components/editor/file-tree/file-viewer.tsx +76 -50
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
- package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
- package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
- package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +198 -0
- package/src/components/slides/reveal-component.tsx +25 -19
- package/src/components/slides/slide-cell-view.tsx +1 -1
- package/src/components/slides/slide-scroll-hint.tsx +161 -0
- package/src/components/slides/slides.css +4 -2
- package/src/components/static-html/share-modal.tsx +1 -1
- package/src/components/ui/input.tsx +1 -1
- package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
- package/src/core/codemirror/find-replace/navigate.ts +2 -0
- package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
- package/src/core/codemirror/go-to-definition/commands.ts +3 -0
- package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
- package/src/core/codemirror/keymaps/keymaps.ts +12 -0
- package/src/core/codemirror/language/__tests__/sql.test.ts +443 -61
- package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
- package/src/core/codemirror/language/languages/sql/sql.ts +210 -3
- package/src/core/codemirror/utils.ts +28 -0
- package/src/core/hotkeys/hotkeys.ts +0 -2
- package/src/core/islands/__tests__/bridge.test.ts +107 -0
- package/src/core/islands/bridge.ts +16 -0
- package/src/core/mode.ts +10 -0
- package/src/core/network/__tests__/api.test.ts +42 -0
- package/src/core/network/__tests__/connection.test.ts +45 -0
- package/src/core/network/__tests__/requests-lazy.test.ts +32 -1
- package/src/core/network/__tests__/requests-network.test.ts +21 -0
- package/src/core/network/api.ts +27 -0
- package/src/core/network/connection.ts +15 -0
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +14 -8
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +25 -7
- package/src/core/static/download-html.ts +5 -14
- package/src/core/wasm/bridge.ts +4 -2
- package/src/core/wasm/worker/types.ts +5 -2
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
- package/src/utils/__tests__/download.test.tsx +24 -1
- package/src/utils/download.ts +11 -6
- package/dist/assets/RenderHTML-BCvU9dYz.js +0 -1
- package/dist/assets/add-connection-dialog-B-jCRNTN.js +0 -170
- package/dist/assets/ai-model-dropdown-DXgLphFa.js +0 -5
- package/dist/assets/cell-link-DqBkyAFe.js +0 -1
- package/dist/assets/cells-Cf7bHHq3.js +0 -229
- package/dist/assets/components-BH6gDVv6.js +0 -1
- package/dist/assets/config-BbOTTCYY.js +0 -1
- package/dist/assets/dist-BCMBu0rO.js +0 -2
- package/dist/assets/download-BomYt3nl.js +0 -9
- package/dist/assets/file-explorer-panel-BfXM2BOd.js +0 -26
- package/dist/assets/floating-outline-wz-WSRgP.js +0 -1
- package/dist/assets/focus-DiEyfiAG.js +0 -1
- package/dist/assets/home-page-DJ4jJsZj.js +0 -4
- package/dist/assets/index-A71LAt6h.css +0 -2
- package/dist/assets/index-Dbc5mg7a.js +0 -38
- package/dist/assets/kiosk-mode-ABm3oYgq.js +0 -1
- package/dist/assets/name-cell-input-C6cukQ9k.js +0 -1
- package/dist/assets/node-sql-parser-BWgUjcfV.js +0 -68
- package/dist/assets/outline-panel-BPlg9_CE.js +0 -1
- package/dist/assets/panels-Dj45fW-v.js +0 -4
- package/dist/assets/process-output-BEhIGfkO.js +0 -1
- package/dist/assets/readonly-code-display-D63oRtBP.js +0 -1
- package/dist/assets/scratchpad-panel-B0mEqBNk.js +0 -1
- package/dist/assets/slides-MCbMDtlY.css +0 -1
- package/dist/assets/state-BZQc7xxL.js +0 -3
- package/dist/assets/switch-CvweaCXq.js +0 -1
- package/dist/assets/useCellActionButton-CRfIEVB5.js +0 -1
- package/dist/assets/useEventListener-B_w3A283.js +0 -1
- package/dist/assets/useNotebookActions-DTXAoukG.js +0 -12
- package/dist/assets/useRunCells-CSSF_mJ0.js +0 -1
|
@@ -12,12 +12,17 @@ import {
|
|
|
12
12
|
type ViewUpdate,
|
|
13
13
|
} from "@codemirror/view";
|
|
14
14
|
import {
|
|
15
|
+
aliasColumnCompletionSource,
|
|
16
|
+
createCteCompletionSource,
|
|
15
17
|
defaultSqlHoverTheme,
|
|
16
18
|
NodeSqlParser,
|
|
17
19
|
type NodeSqlParserResult,
|
|
20
|
+
QueryContextAnalyzer,
|
|
18
21
|
type SupportedDialects as ParserDialects,
|
|
19
22
|
type SqlParseError,
|
|
20
23
|
sqlExtension,
|
|
24
|
+
SqlStructureAnalyzer,
|
|
25
|
+
unqualifiedColumnCompletionSource,
|
|
21
26
|
} from "@marimo-team/codemirror-sql";
|
|
22
27
|
import { DuckDBDialect } from "@marimo-team/codemirror-sql/dialects";
|
|
23
28
|
import { type SQLMetadata, SQLParser } from "@marimo-team/smart-cells";
|
|
@@ -142,6 +147,7 @@ export class SQLLanguageAdapter implements LanguageAdapter<SQLLanguageAdapterMet
|
|
|
142
147
|
_placeholderType: PlaceholderType,
|
|
143
148
|
lspConfig: LSPConfig & { diagnostics: DiagnosticsConfig },
|
|
144
149
|
): Extension[] {
|
|
150
|
+
const analysis = createSQLAnalysis();
|
|
145
151
|
const extensions = [
|
|
146
152
|
// This can be updated with a dispatch effect
|
|
147
153
|
sqlConfigCompartment.of(sql({ dialect: DEFAULT_DIALECT })),
|
|
@@ -167,6 +173,10 @@ export class SQLLanguageAdapter implements LanguageAdapter<SQLLanguageAdapterMet
|
|
|
167
173
|
variableCompletionSource,
|
|
168
174
|
// Completions for dialect keywords
|
|
169
175
|
customKeywordCompletionSource(),
|
|
176
|
+
// Completions based on the current query
|
|
177
|
+
analysis.cteCompletionSource,
|
|
178
|
+
analysis.aliasCompletionSource,
|
|
179
|
+
analysis.columnCompletionSource,
|
|
170
180
|
],
|
|
171
181
|
}),
|
|
172
182
|
];
|
|
@@ -185,10 +195,23 @@ export class SQLLanguageAdapter implements LanguageAdapter<SQLLanguageAdapterMet
|
|
|
185
195
|
|
|
186
196
|
extensions.push(
|
|
187
197
|
sqlExtension({
|
|
198
|
+
schema: getSchema,
|
|
188
199
|
enableLinting: true,
|
|
189
200
|
linterConfig: {
|
|
190
201
|
delay: 250, // Delay before running validation
|
|
191
202
|
parser: parser,
|
|
203
|
+
// CustomSqlParser performs backend DuckDB validation in validateSql.
|
|
204
|
+
perStatement: false,
|
|
205
|
+
},
|
|
206
|
+
enableSemanticLinting: true,
|
|
207
|
+
semanticLinterConfig: {
|
|
208
|
+
parser: analysis.parser,
|
|
209
|
+
structureAnalyzer: analysis.structureAnalyzer,
|
|
210
|
+
severity: {
|
|
211
|
+
unknownTable: "off",
|
|
212
|
+
unknownColumn: "warning",
|
|
213
|
+
ambiguousColumn: "warning",
|
|
214
|
+
},
|
|
192
215
|
},
|
|
193
216
|
enableGutterMarkers: true,
|
|
194
217
|
gutterConfig: {
|
|
@@ -198,14 +221,21 @@ export class SQLLanguageAdapter implements LanguageAdapter<SQLLanguageAdapterMet
|
|
|
198
221
|
parser: parser,
|
|
199
222
|
},
|
|
200
223
|
hoverConfig: {
|
|
201
|
-
schema: getSchema, // Use the same schema as autocomplete
|
|
202
224
|
hoverTime: 300, // 300ms hover delay
|
|
203
225
|
enableKeywords: true, // Show keyword information
|
|
204
226
|
enableTables: true, // Show table information
|
|
205
227
|
enableColumns: true, // Show column information
|
|
206
|
-
parser: parser,
|
|
228
|
+
parser: analysis.parser,
|
|
229
|
+
contextAnalyzer: analysis.contextAnalyzer,
|
|
207
230
|
theme: defaultSqlHoverTheme(theme),
|
|
208
231
|
},
|
|
232
|
+
enableNavigation: true,
|
|
233
|
+
navigationConfig: {
|
|
234
|
+
contextAnalyzer: analysis.contextAnalyzer,
|
|
235
|
+
keymap: false,
|
|
236
|
+
parser: analysis.parser,
|
|
237
|
+
structureAnalyzer: analysis.structureAnalyzer,
|
|
238
|
+
},
|
|
209
239
|
}),
|
|
210
240
|
EditorView.updateListener.of((update) => {
|
|
211
241
|
if (update.focusChanged) {
|
|
@@ -223,6 +253,123 @@ export class SQLLanguageAdapter implements LanguageAdapter<SQLLanguageAdapterMet
|
|
|
223
253
|
}
|
|
224
254
|
}
|
|
225
255
|
|
|
256
|
+
function createSQLAnalysis() {
|
|
257
|
+
const parser = new NodeSqlParser({
|
|
258
|
+
getParserOptions: (state: EditorState) => {
|
|
259
|
+
return {
|
|
260
|
+
database: getAnalysisDialect(state),
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
const contextAnalyzer = new DialectAwareQueryContextAnalyzer(parser);
|
|
265
|
+
const structureAnalyzer = new DialectAwareSqlStructureAnalyzer(parser);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
parser,
|
|
269
|
+
contextAnalyzer,
|
|
270
|
+
structureAnalyzer,
|
|
271
|
+
cteCompletionSource: createCteCompletionSource({
|
|
272
|
+
parser,
|
|
273
|
+
contextAnalyzer,
|
|
274
|
+
structureAnalyzer,
|
|
275
|
+
}),
|
|
276
|
+
aliasCompletionSource: aliasColumnCompletionSource({
|
|
277
|
+
schema: getSchema,
|
|
278
|
+
parser,
|
|
279
|
+
contextAnalyzer,
|
|
280
|
+
}),
|
|
281
|
+
columnCompletionSource: unqualifiedColumnCompletionSource({
|
|
282
|
+
schema: getSchema,
|
|
283
|
+
parser,
|
|
284
|
+
contextAnalyzer,
|
|
285
|
+
}),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getAnalysisDialect(state: EditorState): ParserDialects {
|
|
290
|
+
return guessParserDialect(state) ?? DEFAULT_PARSER_DIALECT;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
class DialectAwareQueryContextAnalyzer extends QueryContextAnalyzer {
|
|
294
|
+
private readonly analyzers = new Map<ParserDialects, QueryContextAnalyzer>();
|
|
295
|
+
private readonly analysisParser: NodeSqlParser;
|
|
296
|
+
|
|
297
|
+
constructor(analysisParser: NodeSqlParser) {
|
|
298
|
+
super(analysisParser);
|
|
299
|
+
this.analysisParser = analysisParser;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
override getContext(
|
|
303
|
+
sql: string,
|
|
304
|
+
opts: { state: EditorState },
|
|
305
|
+
): ReturnType<QueryContextAnalyzer["getContext"]> {
|
|
306
|
+
return this.getAnalyzer(opts.state).getContext(sql, opts);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
override clearCache(): void {
|
|
310
|
+
for (const analyzer of this.analyzers.values()) {
|
|
311
|
+
analyzer.clearCache();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private getAnalyzer(state: EditorState): QueryContextAnalyzer {
|
|
316
|
+
const dialect = getAnalysisDialect(state);
|
|
317
|
+
let analyzer = this.analyzers.get(dialect);
|
|
318
|
+
if (!analyzer) {
|
|
319
|
+
analyzer = new QueryContextAnalyzer(this.analysisParser);
|
|
320
|
+
this.analyzers.set(dialect, analyzer);
|
|
321
|
+
}
|
|
322
|
+
return analyzer;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
class DialectAwareSqlStructureAnalyzer extends SqlStructureAnalyzer {
|
|
327
|
+
private readonly analyzers = new Map<ParserDialects, SqlStructureAnalyzer>();
|
|
328
|
+
private readonly analysisParser: NodeSqlParser;
|
|
329
|
+
|
|
330
|
+
constructor(analysisParser: NodeSqlParser) {
|
|
331
|
+
super(analysisParser);
|
|
332
|
+
this.analysisParser = analysisParser;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
override analyzeDocument(
|
|
336
|
+
state: EditorState,
|
|
337
|
+
): ReturnType<SqlStructureAnalyzer["analyzeDocument"]> {
|
|
338
|
+
return this.getAnalyzer(state).analyzeDocument(state);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
override getStatementAtPosition(
|
|
342
|
+
state: EditorState,
|
|
343
|
+
position: number,
|
|
344
|
+
): ReturnType<SqlStructureAnalyzer["getStatementAtPosition"]> {
|
|
345
|
+
return this.getAnalyzer(state).getStatementAtPosition(state, position);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
override getStatementsInRange(
|
|
349
|
+
state: EditorState,
|
|
350
|
+
from: number,
|
|
351
|
+
to: number,
|
|
352
|
+
): ReturnType<SqlStructureAnalyzer["getStatementsInRange"]> {
|
|
353
|
+
return this.getAnalyzer(state).getStatementsInRange(state, from, to);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
override clearCache(): void {
|
|
357
|
+
for (const analyzer of this.analyzers.values()) {
|
|
358
|
+
analyzer.clearCache();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private getAnalyzer(state: EditorState): SqlStructureAnalyzer {
|
|
363
|
+
const dialect = getAnalysisDialect(state);
|
|
364
|
+
let analyzer = this.analyzers.get(dialect);
|
|
365
|
+
if (!analyzer) {
|
|
366
|
+
analyzer = new SqlStructureAnalyzer(this.analysisParser);
|
|
367
|
+
this.analyzers.set(dialect, analyzer);
|
|
368
|
+
}
|
|
369
|
+
return analyzer;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
226
373
|
class CustomSqlParser extends NodeSqlParser {
|
|
227
374
|
private validationTimeout: number | null = null;
|
|
228
375
|
private readonly VALIDATION_DELAY_MS = 300; // Wait 300ms after user stops typing
|
|
@@ -344,7 +491,61 @@ function getSchema(view: EditorView): SQLNamespace {
|
|
|
344
491
|
return {};
|
|
345
492
|
}
|
|
346
493
|
|
|
347
|
-
|
|
494
|
+
const schema = config.schema;
|
|
495
|
+
if (!isNamespaceMap(schema)) {
|
|
496
|
+
return schema;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const defaultSchema = config.defaultSchema;
|
|
500
|
+
const defaultSchemaNamespace = defaultSchema
|
|
501
|
+
? schema[defaultSchema]
|
|
502
|
+
: undefined;
|
|
503
|
+
|
|
504
|
+
// The completion schema contains both default-schema and fully qualified
|
|
505
|
+
// paths. Promote default-schema tables so unqualified references resolve to
|
|
506
|
+
// one exact match instead of appearing ambiguous.
|
|
507
|
+
if (
|
|
508
|
+
defaultSchemaNamespace &&
|
|
509
|
+
!Array.isArray(defaultSchemaNamespace) &&
|
|
510
|
+
typeof defaultSchemaNamespace === "object" &&
|
|
511
|
+
"children" in defaultSchemaNamespace &&
|
|
512
|
+
isNamespaceMap(defaultSchemaNamespace.children)
|
|
513
|
+
) {
|
|
514
|
+
return {
|
|
515
|
+
...schema,
|
|
516
|
+
...Object.fromEntries(
|
|
517
|
+
Object.entries(defaultSchemaNamespace.children).filter(
|
|
518
|
+
([name]) => !Object.hasOwn(schema, name),
|
|
519
|
+
),
|
|
520
|
+
),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return schema;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function isNamespaceMap(
|
|
528
|
+
namespace: SQLNamespace,
|
|
529
|
+
): namespace is Record<string, SQLNamespace> {
|
|
530
|
+
if (Array.isArray(namespace)) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (!("self" in namespace) || !("children" in namespace)) {
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// A self/children wrapper stores a Completion in `self`. A table or schema
|
|
539
|
+
// literally named "self" stores another namespace there.
|
|
540
|
+
const self = namespace.self;
|
|
541
|
+
const isSelfChildrenWrapper =
|
|
542
|
+
typeof self === "object" &&
|
|
543
|
+
self !== null &&
|
|
544
|
+
!Array.isArray(self) &&
|
|
545
|
+
"label" in self &&
|
|
546
|
+
typeof self.label === "string";
|
|
547
|
+
|
|
548
|
+
return !isSelfChildrenWrapper;
|
|
348
549
|
}
|
|
349
550
|
|
|
350
551
|
function guessParserDialect(state: EditorState): ParserDialects | null {
|
|
@@ -529,3 +730,9 @@ async function validateSQL(
|
|
|
529
730
|
}
|
|
530
731
|
return result;
|
|
531
732
|
}
|
|
733
|
+
|
|
734
|
+
export const exportedForTesting = {
|
|
735
|
+
createSQLAnalysis,
|
|
736
|
+
CustomSqlParser,
|
|
737
|
+
getSchema,
|
|
738
|
+
};
|
|
@@ -52,6 +52,34 @@ export function focusInputAndMoveToEnd(
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Scrolls the `.marimo-cell` that owns `view` into view.
|
|
57
|
+
*
|
|
58
|
+
* `EditorView.scrollIntoView` doesn't work in columns mode: it stops at the
|
|
59
|
+
* first overflowing ancestor even if that ancestor doesn't actually scroll,
|
|
60
|
+
* so it never reaches the real horizontally-scrolling container. The
|
|
61
|
+
* browser's native `Element.scrollIntoView` doesn't have that problem.
|
|
62
|
+
*
|
|
63
|
+
* Deferred a frame so CodeMirror's own scroll settles first; otherwise it
|
|
64
|
+
* clamps this scroll right back.
|
|
65
|
+
*
|
|
66
|
+
* https://github.com/marimo-team/marimo/issues/10222
|
|
67
|
+
*/
|
|
68
|
+
export function scrollOwnerCell(view: EditorView): void {
|
|
69
|
+
const cell = view.dom.closest(".marimo-cell");
|
|
70
|
+
if (!cell) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
requestAnimationFrame(() => {
|
|
75
|
+
cell.scrollIntoView({
|
|
76
|
+
behavior: "instant",
|
|
77
|
+
block: "nearest",
|
|
78
|
+
inline: "nearest",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
55
83
|
export function isInVimMode(ev: EditorView): boolean {
|
|
56
84
|
return getCM(ev)?.state.vim != null;
|
|
57
85
|
}
|
|
@@ -256,13 +256,11 @@ const DEFAULT_HOT_KEY = {
|
|
|
256
256
|
name: "Copy line(s) up",
|
|
257
257
|
group: "Editing",
|
|
258
258
|
key: "Alt-Shift-ArrowUp",
|
|
259
|
-
editable: false,
|
|
260
259
|
},
|
|
261
260
|
"cell.copyLineDown": {
|
|
262
261
|
name: "Copy line(s) down",
|
|
263
262
|
group: "Editing",
|
|
264
263
|
key: "Alt-Shift-ArrowDown",
|
|
265
|
-
editable: false,
|
|
266
264
|
},
|
|
267
265
|
|
|
268
266
|
// Markdown
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "@/__tests__/branded";
|
|
11
11
|
import { notebookAtom } from "@/core/cells/cells";
|
|
12
12
|
import { islandsPendingInitialRunsAtom } from "@/core/islands/state";
|
|
13
|
+
import type { IslandsKernelMessage } from "../worker/worker";
|
|
13
14
|
|
|
14
15
|
type Base64String = components["schemas"]["Base64String"];
|
|
15
16
|
interface TestIslandApp {
|
|
@@ -173,6 +174,19 @@ describe("IslandsPyodideBridge", () => {
|
|
|
173
174
|
});
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
function sendKernelMessage(sessionGeneration: number) {
|
|
178
|
+
const listener = mockMessageListeners.get("kernelMessage");
|
|
179
|
+
if (!listener) {
|
|
180
|
+
throw new Error("Missing kernel message listener");
|
|
181
|
+
}
|
|
182
|
+
const message = {
|
|
183
|
+
message: "" as IslandsKernelMessage["message"],
|
|
184
|
+
sessionGeneration,
|
|
185
|
+
};
|
|
186
|
+
listener(message as never);
|
|
187
|
+
return message;
|
|
188
|
+
}
|
|
189
|
+
|
|
176
190
|
async function initializeSingleApp() {
|
|
177
191
|
mockSingleApp();
|
|
178
192
|
await bridge.initializeApps();
|
|
@@ -281,6 +295,99 @@ describe("IslandsPyodideBridge", () => {
|
|
|
281
295
|
});
|
|
282
296
|
|
|
283
297
|
describe("app lifecycle", () => {
|
|
298
|
+
it("ignores messages after a session yields ownership", async () => {
|
|
299
|
+
const consumeMessage = vi.fn();
|
|
300
|
+
bridge.consumeMessages(consumeMessage);
|
|
301
|
+
mockSingleApp();
|
|
302
|
+
await bridge.initializeApps();
|
|
303
|
+
|
|
304
|
+
const firstMessage = sendKernelMessage(1);
|
|
305
|
+
let finishReplacement!: () => void;
|
|
306
|
+
mockReplaceSessionRequest.mockReturnValueOnce(
|
|
307
|
+
new Promise<void>((resolve) => {
|
|
308
|
+
finishReplacement = resolve;
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
setSingleApp("generated app 2");
|
|
312
|
+
const replacement = bridge.initializeApps();
|
|
313
|
+
await vi.waitFor(() =>
|
|
314
|
+
expect(mockReplaceSessionRequest).toHaveBeenCalledOnce(),
|
|
315
|
+
);
|
|
316
|
+
sendKernelMessage(1);
|
|
317
|
+
const currentMessage = sendKernelMessage(2);
|
|
318
|
+
|
|
319
|
+
expect(consumeMessage.mock.calls).toEqual([
|
|
320
|
+
[firstMessage],
|
|
321
|
+
[currentMessage],
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
finishReplacement();
|
|
325
|
+
await replacement;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("restores message ownership when replacement fails", async () => {
|
|
329
|
+
const consumeMessage = vi.fn();
|
|
330
|
+
bridge.consumeMessages(consumeMessage);
|
|
331
|
+
mockSingleApp();
|
|
332
|
+
await bridge.initializeApps();
|
|
333
|
+
mockReplaceSessionRequest.mockRejectedValueOnce(
|
|
334
|
+
new Error("replacement failed"),
|
|
335
|
+
);
|
|
336
|
+
setSingleApp("generated app 2");
|
|
337
|
+
|
|
338
|
+
await expect(bridge.initializeApps()).rejects.toThrow(
|
|
339
|
+
"replacement failed",
|
|
340
|
+
);
|
|
341
|
+
const previousMessage = sendKernelMessage(1);
|
|
342
|
+
sendKernelMessage(2);
|
|
343
|
+
|
|
344
|
+
expect(consumeMessage.mock.calls).toEqual([[previousMessage]]);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("ignores messages while their session is stopping", async () => {
|
|
348
|
+
const consumeMessage = vi.fn();
|
|
349
|
+
bridge.consumeMessages(consumeMessage);
|
|
350
|
+
mockSingleApp();
|
|
351
|
+
await bridge.initializeApps();
|
|
352
|
+
let finishStop!: () => void;
|
|
353
|
+
mockStopSessionRequest.mockReturnValueOnce(
|
|
354
|
+
new Promise<void>((resolve) => {
|
|
355
|
+
finishStop = resolve;
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const stop = bridge.stopSession();
|
|
360
|
+
await vi.waitFor(() =>
|
|
361
|
+
expect(mockStopSessionRequest).toHaveBeenCalledOnce(),
|
|
362
|
+
);
|
|
363
|
+
sendKernelMessage(1);
|
|
364
|
+
|
|
365
|
+
expect(consumeMessage).not.toHaveBeenCalled();
|
|
366
|
+
|
|
367
|
+
finishStop();
|
|
368
|
+
await stop;
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("forwards messages from every app in a multi-app document", async () => {
|
|
372
|
+
const consumeMessage = vi.fn();
|
|
373
|
+
bridge.consumeMessages(consumeMessage);
|
|
374
|
+
mockParseMarimoIslandApps.mockReturnValue([app(), app("app-2")]);
|
|
375
|
+
mockCreateMarimoFile
|
|
376
|
+
.mockReturnValueOnce("generated app 1")
|
|
377
|
+
.mockReturnValueOnce("generated app 2");
|
|
378
|
+
signalWorkerReady();
|
|
379
|
+
|
|
380
|
+
await bridge.initializeApps();
|
|
381
|
+
const firstMessage = sendKernelMessage(1);
|
|
382
|
+
const secondMessage = sendKernelMessage(2);
|
|
383
|
+
sendKernelMessage(3);
|
|
384
|
+
|
|
385
|
+
expect(consumeMessage.mock.calls).toEqual([
|
|
386
|
+
[firstMessage],
|
|
387
|
+
[secondMessage],
|
|
388
|
+
]);
|
|
389
|
+
});
|
|
390
|
+
|
|
284
391
|
it("holds controls until the first app session is ready", async () => {
|
|
285
392
|
setSingleApp();
|
|
286
393
|
|
|
@@ -66,6 +66,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
66
66
|
private session: AppSession | undefined;
|
|
67
67
|
private sessionReady = new Deferred<AppSession>();
|
|
68
68
|
private nextSessionGeneration = 0;
|
|
69
|
+
private readonly activeSessionGenerations = new Set<number>();
|
|
69
70
|
private appTransition = Promise.resolve();
|
|
70
71
|
private workerReady = new Deferred<void>();
|
|
71
72
|
|
|
@@ -110,6 +111,11 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
110
111
|
);
|
|
111
112
|
|
|
112
113
|
this.rpc.addMessageListener("kernelMessage", (message) => {
|
|
114
|
+
// A stopped session can still have messages in transit after the next
|
|
115
|
+
// app takes ownership of the shared frontend state.
|
|
116
|
+
if (!this.activeSessionGenerations.has(message.sessionGeneration)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
113
119
|
this.messageConsumer?.(message);
|
|
114
120
|
});
|
|
115
121
|
}
|
|
@@ -149,6 +155,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
149
155
|
return;
|
|
150
156
|
}
|
|
151
157
|
if (apps.length > 0) {
|
|
158
|
+
this.activeSessionGenerations.clear();
|
|
152
159
|
this.store.set(
|
|
153
160
|
islandsPendingInitialRunsAtom,
|
|
154
161
|
new Set(apps.map((_, index) => this.nextSessionGeneration + index + 1)),
|
|
@@ -165,6 +172,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
165
172
|
};
|
|
166
173
|
const previousSession = this.session;
|
|
167
174
|
const replacesSession = managesSingleApp && previousSession !== undefined;
|
|
175
|
+
this.activeSessionGenerations.add(request.sessionGeneration);
|
|
168
176
|
if (replacesSession) {
|
|
169
177
|
this.store.set(notebookAtom, initialNotebookState());
|
|
170
178
|
}
|
|
@@ -186,8 +194,14 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
186
194
|
this.sessionReady.resolve(this.session);
|
|
187
195
|
}
|
|
188
196
|
} catch (error) {
|
|
197
|
+
this.activeSessionGenerations.delete(request.sessionGeneration);
|
|
189
198
|
if (this.session?.sessionGeneration === request.sessionGeneration) {
|
|
190
199
|
this.session = previousSession;
|
|
200
|
+
if (previousSession) {
|
|
201
|
+
this.activeSessionGenerations.add(
|
|
202
|
+
previousSession.sessionGeneration,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
191
205
|
}
|
|
192
206
|
Logger.error(`Failed to start session for app ${app.id}:`, error);
|
|
193
207
|
if (managesSingleApp) {
|
|
@@ -203,6 +217,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
203
217
|
if (session?.code === undefined || (appId && session.appId !== appId)) {
|
|
204
218
|
return;
|
|
205
219
|
}
|
|
220
|
+
this.activeSessionGenerations.delete(session.sessionGeneration);
|
|
206
221
|
await this.rpc.proxy.request.stopSession({
|
|
207
222
|
appId: session.appId,
|
|
208
223
|
sessionGeneration: session.sessionGeneration,
|
|
@@ -339,6 +354,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
339
354
|
sendUpdateFile = throwNotImplemented;
|
|
340
355
|
sendFileDetails = throwNotImplemented;
|
|
341
356
|
openTutorial = throwNotImplemented;
|
|
357
|
+
getExportAvailability = throwNotImplemented;
|
|
342
358
|
exportAsHTML = throwNotImplemented;
|
|
343
359
|
exportAsIPYNB = throwNotImplemented;
|
|
344
360
|
exportAsMarkdown = throwNotImplemented;
|
package/src/core/mode.ts
CHANGED
|
@@ -78,6 +78,16 @@ export const viewStateAtom = atom<ViewState>({
|
|
|
78
78
|
|
|
79
79
|
export const initialModeAtom = atom<AppMode | undefined>(undefined);
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Whether the current page hosts a notebook, and so connects to a kernel.
|
|
83
|
+
* False for non-notebook pages (home, gallery), which are served without a
|
|
84
|
+
* session.
|
|
85
|
+
*/
|
|
86
|
+
export function isNotebookPage(): boolean {
|
|
87
|
+
const mode = store.get(initialModeAtom);
|
|
88
|
+
return mode !== "home" && mode !== "gallery";
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
export const kioskModeAtom = atom<boolean>(false);
|
|
82
92
|
|
|
83
93
|
/**
|
|
@@ -94,4 +94,46 @@ describe("API", () => {
|
|
|
94
94
|
}),
|
|
95
95
|
).rejects.toBeInstanceOf(HTTPError);
|
|
96
96
|
});
|
|
97
|
+
|
|
98
|
+
it("preserves export metadata from response headers", async () => {
|
|
99
|
+
const response = new Response("markdown", {
|
|
100
|
+
headers: {
|
|
101
|
+
"Content-Disposition":
|
|
102
|
+
"attachment; filename*=UTF-8''r%C3%A9sum%C3%A9.qmd",
|
|
103
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await expect(
|
|
108
|
+
API.handleExportResponse({ data: "markdown", response }),
|
|
109
|
+
).resolves.toEqual({
|
|
110
|
+
contents: "markdown",
|
|
111
|
+
filename: "résumé.qmd",
|
|
112
|
+
mediaType: "text/plain; charset=utf-8",
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("rejects export responses without a filename", async () => {
|
|
117
|
+
await expect(
|
|
118
|
+
API.handleExportResponse({
|
|
119
|
+
data: "markdown",
|
|
120
|
+
response: new Response(null, {
|
|
121
|
+
headers: { "Content-Type": "text/plain" },
|
|
122
|
+
}),
|
|
123
|
+
}),
|
|
124
|
+
).rejects.toThrow("Export response is missing a filename");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("rejects export responses without a media type", async () => {
|
|
128
|
+
await expect(
|
|
129
|
+
API.handleExportResponse({
|
|
130
|
+
data: "markdown",
|
|
131
|
+
response: new Response(null, {
|
|
132
|
+
headers: {
|
|
133
|
+
"Content-Disposition": "attachment; filename*=UTF-8''notebook.md",
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
}),
|
|
137
|
+
).rejects.toThrow("Export response is missing a media type");
|
|
138
|
+
});
|
|
97
139
|
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { initialModeAtom } from "@/core/mode";
|
|
5
|
+
import { store } from "@/core/state/jotai";
|
|
6
|
+
import { WebSocketState } from "../../websocket/types";
|
|
7
|
+
import { connectionAtom, waitForConnectionOpenIfNotebook } from "../connection";
|
|
8
|
+
|
|
9
|
+
const NEVER = Symbol("never");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolves to NEVER if the promise does not settle within a macrotask.
|
|
13
|
+
*/
|
|
14
|
+
function settledOrNever(promise: Promise<unknown>) {
|
|
15
|
+
return Promise.race([
|
|
16
|
+
promise,
|
|
17
|
+
new Promise((resolve) => setTimeout(() => resolve(NEVER), 0)),
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("waitForConnectionOpenIfNotebook", () => {
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
store.set(connectionAtom, { state: WebSocketState.NOT_STARTED });
|
|
24
|
+
store.set(initialModeAtom, undefined);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it.each(["home", "gallery"] as const)(
|
|
28
|
+
"resolves without a connection in %s mode",
|
|
29
|
+
async (mode) => {
|
|
30
|
+
store.set(initialModeAtom, mode);
|
|
31
|
+
await expect(
|
|
32
|
+
settledOrNever(waitForConnectionOpenIfNotebook()),
|
|
33
|
+
).resolves.not.toBe(NEVER);
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
it("waits for the connection to open in edit mode", async () => {
|
|
38
|
+
store.set(initialModeAtom, "edit");
|
|
39
|
+
const promise = waitForConnectionOpenIfNotebook();
|
|
40
|
+
await expect(settledOrNever(promise)).resolves.toBe(NEVER);
|
|
41
|
+
|
|
42
|
+
store.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
43
|
+
await expect(settledOrNever(promise)).resolves.not.toBe(NEVER);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -4,7 +4,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
4
4
|
import { cellId, requestId, uiElementId } from "@/__tests__/branded";
|
|
5
5
|
import type { RuntimeManager } from "../../runtime/runtime";
|
|
6
6
|
import { createLazyRequests } from "../requests-lazy";
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
EditRequests,
|
|
9
|
+
ExportAvailabilityResponse,
|
|
10
|
+
RunRequests,
|
|
11
|
+
} from "../types";
|
|
8
12
|
|
|
9
13
|
// Mock the connection module
|
|
10
14
|
vi.mock("../connection", async () => {
|
|
@@ -72,6 +76,33 @@ describe("createLazyRequests", () => {
|
|
|
72
76
|
expect(mockDelegate.getEnvironmentInfo).toHaveBeenCalledOnce();
|
|
73
77
|
});
|
|
74
78
|
|
|
79
|
+
it("gets export availability without starting a kernel", async () => {
|
|
80
|
+
const availability: ExportAvailabilityResponse = {
|
|
81
|
+
source: "server",
|
|
82
|
+
formats: [
|
|
83
|
+
{
|
|
84
|
+
format: "ipynb",
|
|
85
|
+
dependenciesAvailable: false,
|
|
86
|
+
missingPackages: ["nbformat"],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
mockDelegate.getExportAvailability = vi
|
|
91
|
+
.fn()
|
|
92
|
+
.mockResolvedValue(availability) as EditRequests["getExportAvailability"];
|
|
93
|
+
|
|
94
|
+
const lazyRequests = createLazyRequests(
|
|
95
|
+
mockDelegate,
|
|
96
|
+
mockGetRuntimeManager,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await expect(lazyRequests.getExportAvailability()).resolves.toEqual(
|
|
100
|
+
availability,
|
|
101
|
+
);
|
|
102
|
+
expect(mockInit).not.toHaveBeenCalled();
|
|
103
|
+
expect(mockDelegate.getExportAvailability).toHaveBeenCalledOnce();
|
|
104
|
+
});
|
|
105
|
+
|
|
75
106
|
it("should call init once before first request", async () => {
|
|
76
107
|
const lazyRequests = createLazyRequests(
|
|
77
108
|
mockDelegate,
|