@marimo-team/frontend 0.25.3-dev104 → 0.25.3-dev108
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-DVrvejMC.js → CellStatus-_Aw9_foI.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-Dft8R6l5.js → ConnectedDataExplorerComponent-BRgqbAsc.js} +1 -1
- package/dist/assets/{ErrorBoundary-0akNKvJm.js → ErrorBoundary-DxSzjvbx.js} +2 -2
- package/dist/assets/{JsonOutput-DeUmpc_f.js → JsonOutput-B7pGj79O.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-BazxeAYV.js → MarimoErrorOutput-Bzo6Bkp5.js} +1 -1
- package/dist/assets/{RenderHTML-CbAdeual.js → RenderHTML-D6AC0IhC.js} +1 -1
- package/dist/assets/{RunButton-CjKwgCFU.js → RunButton-BjYsPiKa.js} +1 -1
- package/dist/assets/{add-cell-with-ai-BKVEws3J.js → add-cell-with-ai-DlpS2-wQ.js} +4 -4
- package/dist/assets/{add-connection-dialog-DS7LTz1T.js → add-connection-dialog-CyNAB_W2.js} +1 -1
- package/dist/assets/{agent-panel-B34QcdOK.js → agent-panel-_lzeDJ1g.js} +1 -1
- package/dist/assets/{ai-model-dropdown-DqFJCFVR.js → ai-model-dropdown-DSTTUxpi.js} +1 -1
- package/dist/assets/{app-config-button-CVh4UKi9.js → app-config-button-ChYKLsLO.js} +1 -1
- package/dist/assets/{blob-Bw0ZQoLx.js → blob-DTYOX-m5.js} +1 -1
- package/dist/assets/{cell-editor-DQ0io5Ye.js → cell-editor-Beg8ahPA.js} +2 -2
- package/dist/assets/{cell-link-DvOIu6A3.js → cell-link-D3rkqgbU.js} +1 -1
- package/dist/assets/{cells-Do-63MKT.js → cells-2HETnlvE.js} +5 -5
- package/dist/assets/{chat-display-DnkhBxdh.js → chat-display-0GIEXLvN.js} +1 -1
- package/dist/assets/{chat-panel-CN3qoX9G.js → chat-panel-BuWn4Z_F.js} +1 -1
- package/dist/assets/{chat-ui-B54K5qqr.js → chat-ui-X0s3vvdf.js} +1 -1
- package/dist/assets/{column-preview-DmxzHzaW.js → column-preview-yI7-INkZ.js} +1 -1
- package/dist/assets/{command-palette-CciCeRhy.js → command-palette-D6WLW1Ie.js} +1 -1
- package/dist/assets/{common-BCyx7t1i.js → common-DgXKwblm.js} +1 -1
- package/dist/assets/{components-CDvWXfXq.js → components-CuMhY4Sf.js} +1 -1
- package/dist/assets/{datasource-DieftweT.js → datasource-D9JMcEAP.js} +1 -1
- package/dist/assets/{dependency-graph-panel-IfcqTHgq.js → dependency-graph-panel-BVJWPCvw.js} +3 -3
- package/dist/assets/{documentation-panel-DeLCgVcq.js → documentation-panel--wI4XHso.js} +1 -1
- package/dist/assets/{download-BzfVCqUq.js → download-Dod42wfD.js} +1 -1
- package/dist/assets/{edit-page-HMyk4cWw.js → edit-page-EmacXgdB.js} +4 -4
- package/dist/assets/{error-entries-DjjWrbt4.js → error-entries-D00nWhWE.js} +1 -1
- package/dist/assets/{error-panel-CiG3JJA3.js → error-panel-CcN_1PWf.js} +1 -1
- package/dist/assets/{feedback-button-B8zVQEi6.js → feedback-button-ByGgqUsX.js} +1 -1
- package/dist/assets/feedback-button-tbWwJ_t6.js +1 -0
- package/dist/assets/{file-explorer-panel-BPD8Q0L-.js → file-explorer-panel-eTEqtMPW.js} +1 -1
- package/dist/assets/{file-name-input-CdkF9K3I.js → file-name-input-D5SfTNWh.js} +1 -1
- package/dist/assets/{floating-outline-TfDWJXBm.js → floating-outline-B-k3P_AO.js} +1 -1
- package/dist/assets/{gallery-page-BJpenPdQ.js → gallery-page-BzbMhDHi.js} +1 -1
- package/dist/assets/{glide-data-editor-DO15Nm-R.js → glide-data-editor-BMqhUrHI.js} +1 -1
- package/dist/assets/{home-page-mvqldGvD.js → home-page-BvA1BCmX.js} +1 -1
- package/dist/assets/{hooks-C4AJfqV0.js → hooks-CaRBg2ni.js} +1 -1
- package/dist/assets/{index-C2XzUsS7.js → index-hwaZTQ9l.js} +9 -9
- package/dist/assets/{layout-1kSh6fBF.js → layout-C-NAerIZ.js} +3 -3
- package/dist/assets/{logs-panel--QbHo7uS.js → logs-panel-B3T1GFBk.js} +1 -1
- package/dist/assets/{markdown-renderer-DJ7H8Rps.js → markdown-renderer-BhiTvxtL.js} +1 -1
- package/dist/assets/{name-cell-input-6Gd19S8T.js → name-cell-input-CgAObR6N.js} +1 -1
- package/dist/assets/{outline-panel-CjKvunvX.js → outline-panel-BhBb7Vqw.js} +1 -1
- package/dist/assets/{packages-panel-Gyrqi-MZ.js → packages-panel-Dz8rOIrh.js} +1 -1
- package/dist/assets/{panel-accordion-state-D49TNCLQ.js → panel-accordion-state-ZsUdpAYn.js} +1 -1
- package/dist/assets/{panels-DfbxoeGa.js → panels-B0lqLcI0.js} +1 -1
- package/dist/assets/{process-output-CgkoeA1-.js → process-output-CeOMwE17.js} +1 -1
- package/dist/assets/{readonly-code-display-BMbl_4BR.js → readonly-code-display-TvOC7ESE.js} +1 -1
- package/dist/assets/{readonly-python-code-BnPfpOJM.js → readonly-python-code-Cqkfdeyo.js} +1 -1
- package/dist/assets/{reveal-component-D84yAzG7.js → reveal-component-y06O6YuH.js} +1 -1
- package/dist/assets/{run-page-B10s8Pre.js → run-page-DHE6ET5K.js} +1 -1
- package/dist/assets/{runtime-details-state-Bkmgb59p.js → runtime-details-state-CKqJSfSR.js} +1 -1
- package/dist/assets/{scratchpad-panel-DtyjJ3yN.js → scratchpad-panel-BW_HfG5l.js} +1 -1
- package/dist/assets/{session-panel-Chaq1do8.js → session-panel-CdHdKUCF.js} +1 -1
- package/dist/assets/{snippets-panel-BaZmddOW.js → snippets-panel-DYLWOLYw.js} +1 -1
- package/dist/assets/{spec-uCpVvaAR.js → spec-DlH4Mp7d.js} +1 -1
- package/dist/assets/{state-Db73TZVD.js → state-DAqdiGqq.js} +1 -1
- package/dist/assets/{state-CqnCwHhY.js → state-DZN9vZ6z.js} +1 -1
- package/dist/assets/{tracing-C2vQMUI0.js → tracing-B0J2ctTm.js} +1 -1
- package/dist/assets/{tracing-panel-B4L2S2ac.js → tracing-panel-C1OCpoeE.js} +2 -2
- package/dist/assets/{tree-actions-JXDiioHK.js → tree-actions-sjBxIglc.js} +1 -1
- package/dist/assets/{useAddCell-CP3HIjGl.js → useAddCell-CaXnFGki.js} +1 -1
- package/dist/assets/{useCellActionButton-yi1_-emd.js → useCellActionButton-CJ90CMEj.js} +1 -1
- package/dist/assets/{useDeleteCell-BPLwyClM.js → useDeleteCell-D9N85822.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-DmBiydXO.js → useDependencyPanelTab-CzAE0cm-.js} +1 -1
- package/dist/assets/{useNotebookActions-CfmgcJGx.js → useNotebookActions-CTKkktJB.js} +1 -1
- package/dist/assets/{useRunCells-jF-7tJkw.js → useRunCells-DGakx8hj.js} +1 -1
- package/dist/assets/{useSplitCell-B6zWL8Zf.js → useSplitCell-AK5BKPGM.js} +1 -1
- package/dist/index.html +21 -21
- package/package.json +2 -2
- package/src/components/data-table/__tests__/chart-spec-model.test.ts +195 -195
- package/src/components/data-table/charts/__tests__/spec-snapshot.test.ts +20 -20
- package/src/components/data-table/charts/__tests__/spec.test.ts +78 -52
- package/src/components/data-table/charts/chart-spec/spec.ts +68 -43
- package/src/components/data-table/charts/charts.tsx +5 -5
- package/src/components/data-table/column-summary/chart-spec-model.tsx +23 -16
- package/src/components/dependency-graph/dependency-graph-tree.tsx +8 -8
- package/src/components/dependency-graph/elements.ts +24 -21
- package/src/components/dependency-graph/utils/__tests__/layout.test.ts +18 -11
- package/src/components/editor/output/ansi-reduce.ts +30 -23
- package/src/components/editor/renderers/grid-layout/grid-layout.tsx +12 -6
- package/src/core/ai/tools/base.ts +16 -7
- package/src/core/ai/tools/edit-notebook-tool.ts +47 -45
- package/src/core/ai/tools/registry.ts +13 -8
- package/src/core/ai/tools/sample-tool.ts +6 -7
- package/src/core/cells/__tests__/session.test.ts +117 -36
- package/src/core/codemirror/__tests__/setup.test.ts +28 -28
- package/src/core/codemirror/language/__tests__/sql.test.ts +22 -20
- package/src/core/codemirror/language/extension.ts +8 -8
- package/src/core/codemirror/language/languages/markdown.ts +8 -6
- package/src/core/codemirror/language/languages/python.ts +13 -7
- package/src/core/codemirror/language/languages/sql/sql.ts +9 -7
- package/src/core/codemirror/language/types.ts +7 -7
- package/src/core/codemirror/rtc/extension.ts +24 -12
- package/src/core/codemirror/rtc/loro/awareness.ts +34 -19
- package/src/core/codemirror/rtc/loro/sync.ts +1 -0
- package/src/core/codemirror/utils.ts +1 -0
- package/src/core/host/__tests__/static.test.ts +167 -0
- package/src/core/host/backend.ts +10 -0
- package/src/core/host/static.ts +70 -0
- package/src/core/islands/components/__tests__/IslandControls.test.tsx +7 -5
- package/src/core/static/__tests__/files.test.ts +38 -10
- package/src/plugins/__tests__/plugin-object-schemas.test.ts +104 -0
- package/src/plugins/impl/DataEditorPlugin.tsx +1 -1
- package/src/plugins/impl/DataTablePlugin.tsx +15 -18
- package/src/plugins/impl/data-frames/DataFramePlugin.tsx +1 -1
- package/src/plugins/impl/plotly/PlotlyPlugin.tsx +2 -5
- package/src/plugins/impl/plotly/selection.ts +25 -19
- package/src/plugins/impl/vega/VegaPlugin.tsx +2 -3
- package/dist/assets/feedback-button-DQdMkSvR.js +0 -1
|
@@ -42,12 +42,14 @@ const adapter = new SQLLanguageAdapter();
|
|
|
42
42
|
const TEST_ENGINE = "test_engine" as ConnectionName;
|
|
43
43
|
|
|
44
44
|
const TEST_EXTENSION_ARGS = [
|
|
45
|
-
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
{
|
|
46
|
+
cellId: {} as CellId,
|
|
47
|
+
completionConfig: {} as CompletionConfig,
|
|
48
|
+
hotkeys: {} as HotkeyProvider,
|
|
49
|
+
placeholderType: {} as PlaceholderType,
|
|
50
|
+
lspConfig: {} as LSPConfig & {
|
|
51
|
+
diagnostics: DiagnosticsConfig;
|
|
52
|
+
},
|
|
51
53
|
},
|
|
52
54
|
] as const;
|
|
53
55
|
|
|
@@ -930,15 +932,15 @@ describe("SQL analysis features", () => {
|
|
|
930
932
|
|
|
931
933
|
it("reports semantic diagnostics as warnings", async () => {
|
|
932
934
|
const adapter = new SQLLanguageAdapter();
|
|
933
|
-
const extensions = adapter.getExtension(
|
|
934
|
-
{} as CellId,
|
|
935
|
-
{} as CompletionConfig,
|
|
936
|
-
{} as HotkeyProvider,
|
|
937
|
-
{} as PlaceholderType,
|
|
938
|
-
{
|
|
935
|
+
const extensions = adapter.getExtension({
|
|
936
|
+
cellId: {} as CellId,
|
|
937
|
+
completionConfig: {} as CompletionConfig,
|
|
938
|
+
hotkeys: {} as HotkeyProvider,
|
|
939
|
+
placeholderType: {} as PlaceholderType,
|
|
940
|
+
lspConfig: {
|
|
939
941
|
diagnostics: { sql_linter: true },
|
|
940
942
|
} as LSPConfig & { diagnostics: DiagnosticsConfig },
|
|
941
|
-
);
|
|
943
|
+
});
|
|
942
944
|
const state = EditorState.create({
|
|
943
945
|
doc: "SELECT missing FROM users",
|
|
944
946
|
extensions: [languageMetadataField.init(() => metadata), ...extensions],
|
|
@@ -963,15 +965,15 @@ describe("SQL analysis features", () => {
|
|
|
963
965
|
|
|
964
966
|
it("does not report unknown tables", async () => {
|
|
965
967
|
const adapter = new SQLLanguageAdapter();
|
|
966
|
-
const extensions = adapter.getExtension(
|
|
967
|
-
{} as CellId,
|
|
968
|
-
{} as CompletionConfig,
|
|
969
|
-
{} as HotkeyProvider,
|
|
970
|
-
{} as PlaceholderType,
|
|
971
|
-
{
|
|
968
|
+
const extensions = adapter.getExtension({
|
|
969
|
+
cellId: {} as CellId,
|
|
970
|
+
completionConfig: {} as CompletionConfig,
|
|
971
|
+
hotkeys: {} as HotkeyProvider,
|
|
972
|
+
placeholderType: {} as PlaceholderType,
|
|
973
|
+
lspConfig: {
|
|
972
974
|
diagnostics: { sql_linter: true },
|
|
973
975
|
} as LSPConfig & { diagnostics: DiagnosticsConfig },
|
|
974
|
-
);
|
|
976
|
+
});
|
|
975
977
|
const state = EditorState.create({
|
|
976
978
|
doc: "SELECT missing FROM users; SELECT * FROM unknown_table",
|
|
977
979
|
extensions: [languageMetadataField.init(() => metadata), ...extensions],
|
|
@@ -167,13 +167,13 @@ function updateLanguageAdapterAndCode({
|
|
|
167
167
|
setLanguageAdapter.of(nextLanguage),
|
|
168
168
|
setLanguageMetadata.of(metadata),
|
|
169
169
|
languageCompartment.reconfigure(
|
|
170
|
-
nextLanguage.getExtension(
|
|
170
|
+
nextLanguage.getExtension({
|
|
171
171
|
cellId,
|
|
172
172
|
completionConfig,
|
|
173
|
-
hotkeysProvider,
|
|
173
|
+
hotkeys: hotkeysProvider,
|
|
174
174
|
placeholderType,
|
|
175
175
|
lspConfig,
|
|
176
|
-
),
|
|
176
|
+
}),
|
|
177
177
|
),
|
|
178
178
|
// Clear history
|
|
179
179
|
historyCompartment.reconfigure([]),
|
|
@@ -218,13 +218,13 @@ export function adaptiveLanguageConfiguration(opts: {
|
|
|
218
218
|
// Language adapter
|
|
219
219
|
languageToggleKeymaps(),
|
|
220
220
|
languageCompartment.of(
|
|
221
|
-
LanguageAdapters.python.getExtension(
|
|
221
|
+
LanguageAdapters.python.getExtension({
|
|
222
222
|
cellId,
|
|
223
223
|
completionConfig,
|
|
224
224
|
hotkeys,
|
|
225
225
|
placeholderType,
|
|
226
226
|
lspConfig,
|
|
227
|
-
),
|
|
227
|
+
}),
|
|
228
228
|
),
|
|
229
229
|
languageAdapterState,
|
|
230
230
|
languageMetadataField,
|
|
@@ -323,12 +323,12 @@ export function reconfigureLanguageEffect(
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
return languageCompartment.reconfigure(
|
|
326
|
-
language.getExtension(
|
|
326
|
+
language.getExtension({
|
|
327
327
|
cellId,
|
|
328
328
|
completionConfig,
|
|
329
|
-
hotkeysProvider,
|
|
329
|
+
hotkeys: hotkeysProvider,
|
|
330
330
|
placeholderType,
|
|
331
331
|
lspConfig,
|
|
332
|
-
),
|
|
332
|
+
}),
|
|
333
333
|
);
|
|
334
334
|
}
|
|
@@ -69,12 +69,14 @@ export class MarkdownLanguageAdapter implements LanguageAdapter<MarkdownLanguage
|
|
|
69
69
|
return this.parser.isSupported(pythonCode);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
public getExtension(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
public getExtension({
|
|
73
|
+
hotkeys,
|
|
74
|
+
}: {
|
|
75
|
+
cellId: CellId;
|
|
76
|
+
completionConfig: CompletionConfig;
|
|
77
|
+
hotkeys: HotkeyProvider;
|
|
78
|
+
placeholderType: PlaceholderType;
|
|
79
|
+
}): Extension[] {
|
|
78
80
|
const markdownLanguageData = markdown().language.data;
|
|
79
81
|
let view: EditorView | undefined;
|
|
80
82
|
|
|
@@ -279,13 +279,19 @@ export class PythonLanguageAdapter implements LanguageAdapter<{}> {
|
|
|
279
279
|
return this.parser.isSupported(code);
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
public getExtension(
|
|
283
|
-
cellId
|
|
284
|
-
completionConfig
|
|
285
|
-
hotkeys
|
|
286
|
-
placeholderType
|
|
287
|
-
lspConfig
|
|
288
|
-
|
|
282
|
+
public getExtension({
|
|
283
|
+
cellId,
|
|
284
|
+
completionConfig,
|
|
285
|
+
hotkeys,
|
|
286
|
+
placeholderType,
|
|
287
|
+
lspConfig,
|
|
288
|
+
}: {
|
|
289
|
+
cellId: CellId;
|
|
290
|
+
completionConfig: CompletionConfig;
|
|
291
|
+
hotkeys: HotkeyProvider;
|
|
292
|
+
placeholderType: PlaceholderType;
|
|
293
|
+
lspConfig: LSPConfig & { diagnostics: DiagnosticsConfig };
|
|
294
|
+
}): Extension[] {
|
|
289
295
|
const getCompletionsExtension = () => {
|
|
290
296
|
const autocompleteOptions = {
|
|
291
297
|
// We remove the default keymap because we use our own which
|
|
@@ -155,13 +155,15 @@ export class SQLLanguageAdapter implements LanguageAdapter<SQLLanguageAdapterMet
|
|
|
155
155
|
return this.parser.isSupported(pythonCode);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
public getExtension(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
158
|
+
public getExtension({
|
|
159
|
+
lspConfig,
|
|
160
|
+
}: {
|
|
161
|
+
cellId: CellId;
|
|
162
|
+
completionConfig: CompletionConfig;
|
|
163
|
+
hotkeys: HotkeyProvider;
|
|
164
|
+
placeholderType: PlaceholderType;
|
|
165
|
+
lspConfig: LSPConfig & { diagnostics: DiagnosticsConfig };
|
|
166
|
+
}): Extension[] {
|
|
165
167
|
const analysis = createSQLAnalysis();
|
|
166
168
|
const extensions = [
|
|
167
169
|
// This can be updated with a dispatch effect
|
|
@@ -26,13 +26,13 @@ export interface LanguageAdapter<M = Record<string, any>> {
|
|
|
26
26
|
transformIn(code: string): [string, number, M];
|
|
27
27
|
transformOut(code: string, metadata: M): [string, number];
|
|
28
28
|
isSupported(code: string): boolean;
|
|
29
|
-
getExtension(
|
|
30
|
-
cellId: CellId
|
|
31
|
-
completionConfig: CompletionConfig
|
|
32
|
-
hotkeys: HotkeyProvider
|
|
33
|
-
placeholderType: PlaceholderType
|
|
34
|
-
lspConfig: LSPConfig & { diagnostics?: DiagnosticsConfig }
|
|
35
|
-
): Extension[];
|
|
29
|
+
getExtension(options: {
|
|
30
|
+
cellId: CellId;
|
|
31
|
+
completionConfig: CompletionConfig;
|
|
32
|
+
hotkeys: HotkeyProvider;
|
|
33
|
+
placeholderType: PlaceholderType;
|
|
34
|
+
lspConfig: LSPConfig & { diagnostics?: DiagnosticsConfig };
|
|
35
|
+
}): Extension[];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export type LanguageMetadataOf<T extends LanguageAdapter> =
|
|
@@ -207,7 +207,13 @@ export function realTimeCollaboration(
|
|
|
207
207
|
extension: [
|
|
208
208
|
languageObserverExtension(cellId),
|
|
209
209
|
languageListenerExtension(cellId),
|
|
210
|
-
loroAwarenessPlugin(
|
|
210
|
+
loroAwarenessPlugin({
|
|
211
|
+
doc,
|
|
212
|
+
awareness,
|
|
213
|
+
user: userState,
|
|
214
|
+
getTextFromDoc: () => loroText,
|
|
215
|
+
cellId,
|
|
216
|
+
}),
|
|
211
217
|
loroSyncPlugin(doc, ["codes", cellId], () => {
|
|
212
218
|
return loroText;
|
|
213
219
|
}),
|
|
@@ -419,14 +425,21 @@ function languageListenerExtension(cellId: CellId) {
|
|
|
419
425
|
/**
|
|
420
426
|
* Create a plugin to observe awareness changes
|
|
421
427
|
*/
|
|
422
|
-
function loroAwarenessPlugin(
|
|
423
|
-
doc
|
|
424
|
-
awareness
|
|
425
|
-
user
|
|
426
|
-
getTextFromDoc
|
|
427
|
-
cellId
|
|
428
|
-
getUserId
|
|
429
|
-
|
|
428
|
+
function loroAwarenessPlugin({
|
|
429
|
+
doc,
|
|
430
|
+
awareness,
|
|
431
|
+
user,
|
|
432
|
+
getTextFromDoc,
|
|
433
|
+
cellId,
|
|
434
|
+
getUserId,
|
|
435
|
+
}: {
|
|
436
|
+
doc: LoroDoc;
|
|
437
|
+
awareness: Awareness<AwarenessState>;
|
|
438
|
+
user: UserState;
|
|
439
|
+
getTextFromDoc: (doc: LoroDoc) => LoroText;
|
|
440
|
+
cellId: CellId;
|
|
441
|
+
getUserId?: () => Uid;
|
|
442
|
+
}): Extension[] {
|
|
430
443
|
const scopeId = `loro:cell:${cellId}` as ScopeId;
|
|
431
444
|
|
|
432
445
|
return [
|
|
@@ -435,15 +448,14 @@ function loroAwarenessPlugin(
|
|
|
435
448
|
createSelectionLayer(),
|
|
436
449
|
ViewPlugin.define(
|
|
437
450
|
(view) =>
|
|
438
|
-
new AwarenessPlugin(
|
|
439
|
-
view,
|
|
451
|
+
new AwarenessPlugin(view, {
|
|
440
452
|
doc,
|
|
441
453
|
user,
|
|
442
454
|
awareness,
|
|
443
455
|
getTextFromDoc,
|
|
444
456
|
scopeId,
|
|
445
457
|
getUserId,
|
|
446
|
-
),
|
|
458
|
+
}),
|
|
447
459
|
),
|
|
448
460
|
ViewPlugin.define(
|
|
449
461
|
(view) => new RemoteAwarenessPlugin(view, doc, awareness, scopeId),
|
|
@@ -208,13 +208,19 @@ export class RemoteCursorMarker implements LayerMarker {
|
|
|
208
208
|
private name: string;
|
|
209
209
|
private colorClassName: string;
|
|
210
210
|
|
|
211
|
-
public constructor(
|
|
212
|
-
left
|
|
213
|
-
top
|
|
214
|
-
height
|
|
215
|
-
name
|
|
216
|
-
colorClassName
|
|
217
|
-
|
|
211
|
+
public constructor({
|
|
212
|
+
left,
|
|
213
|
+
top,
|
|
214
|
+
height,
|
|
215
|
+
name,
|
|
216
|
+
colorClassName,
|
|
217
|
+
}: {
|
|
218
|
+
left: number;
|
|
219
|
+
top: number;
|
|
220
|
+
height: number;
|
|
221
|
+
name: string;
|
|
222
|
+
colorClassName: string;
|
|
223
|
+
}) {
|
|
218
224
|
this.left = left;
|
|
219
225
|
this.top = top;
|
|
220
226
|
this.height = height;
|
|
@@ -271,13 +277,13 @@ export class RemoteCursorMarker implements LayerMarker {
|
|
|
271
277
|
const baseLeft = left - view.scrollDOM.scrollLeft;
|
|
272
278
|
const baseTop = rect.top - view.scrollDOM.scrollTop;
|
|
273
279
|
return [
|
|
274
|
-
new RemoteCursorMarker(
|
|
275
|
-
absolutePosition.left - baseLeft,
|
|
276
|
-
absolutePosition.top - baseTop,
|
|
277
|
-
absolutePosition.bottom - absolutePosition.top,
|
|
278
|
-
displayName,
|
|
280
|
+
new RemoteCursorMarker({
|
|
281
|
+
left: absolutePosition.left - baseLeft,
|
|
282
|
+
top: absolutePosition.top - baseTop,
|
|
283
|
+
height: absolutePosition.bottom - absolutePosition.top,
|
|
284
|
+
name: displayName,
|
|
279
285
|
colorClassName,
|
|
280
|
-
),
|
|
286
|
+
}),
|
|
281
287
|
];
|
|
282
288
|
}
|
|
283
289
|
|
|
@@ -375,12 +381,21 @@ export class AwarenessPlugin implements PluginValue {
|
|
|
375
381
|
|
|
376
382
|
public constructor(
|
|
377
383
|
view: EditorView,
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
+
{
|
|
385
|
+
doc,
|
|
386
|
+
user,
|
|
387
|
+
awareness,
|
|
388
|
+
getTextFromDoc,
|
|
389
|
+
scopeId,
|
|
390
|
+
getUserId,
|
|
391
|
+
}: {
|
|
392
|
+
doc: LoroDoc;
|
|
393
|
+
user: UserState;
|
|
394
|
+
awareness: Awareness<AwarenessState>;
|
|
395
|
+
getTextFromDoc: (doc: LoroDoc) => LoroText;
|
|
396
|
+
scopeId: ScopeId;
|
|
397
|
+
getUserId?: () => Uid;
|
|
398
|
+
},
|
|
384
399
|
) {
|
|
385
400
|
this.view = view;
|
|
386
401
|
this.doc = doc;
|
|
@@ -148,6 +148,7 @@ export class LoroSyncPluginValue implements PluginValue {
|
|
|
148
148
|
return;
|
|
149
149
|
}
|
|
150
150
|
let adj = 0;
|
|
151
|
+
// oxlint-disable-next-line marimo/prefer-object-params -- CodeMirror supplies positional change ranges and inserted text.
|
|
151
152
|
update.changes.iterChanges((fromA, toA, fromB, toB, insert) => {
|
|
152
153
|
const insertText = insert.sliceString(0, insert.length, "\n");
|
|
153
154
|
if (fromA !== toA) {
|
|
@@ -122,6 +122,7 @@ export function hasNewLines(tr: Transaction | ViewUpdate): boolean {
|
|
|
122
122
|
|
|
123
123
|
let hasNewLines = false;
|
|
124
124
|
// Iterate through all changes in the transaction
|
|
125
|
+
// oxlint-disable-next-line marimo/prefer-object-params -- CodeMirror supplies positional change ranges and inserted text.
|
|
125
126
|
tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
|
126
127
|
// Check if the inserted text contains line breaks
|
|
127
128
|
if (inserted.toString().includes("\n")) {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { createNotebookReducer, NotebookEvents } from "@marimo-team/client";
|
|
4
|
+
import * as Protocol from "@marimo-team/client/protocol";
|
|
5
|
+
import { afterEach, assert, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { document as documentResource } from "../resources/document";
|
|
7
|
+
import {
|
|
8
|
+
createStaticNotebookBackend,
|
|
9
|
+
type StaticNotebookSnapshot,
|
|
10
|
+
} from "../static";
|
|
11
|
+
import { fixtureEvents } from "./fixtures";
|
|
12
|
+
|
|
13
|
+
function snapshot(): StaticNotebookSnapshot {
|
|
14
|
+
const events = fixtureEvents("notebook-events.sse");
|
|
15
|
+
const notebook = events.find((event) => event.event === "notebook")?.data
|
|
16
|
+
.notebook;
|
|
17
|
+
const document = events.find((event) => event.event === "document")?.data
|
|
18
|
+
.document;
|
|
19
|
+
assert(notebook && document);
|
|
20
|
+
return {
|
|
21
|
+
notebook,
|
|
22
|
+
document,
|
|
23
|
+
cells: events.flatMap((event) =>
|
|
24
|
+
event.event === "cell" ? [event.data.cell] : [],
|
|
25
|
+
),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const streams: NotebookEvents[] = [];
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
const closed = streams.splice(0);
|
|
32
|
+
closed.forEach((stream) => stream.close());
|
|
33
|
+
await Promise.all(closed.map((stream) => stream.closed));
|
|
34
|
+
vi.useRealTimers();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
async function read(
|
|
38
|
+
backend: ReturnType<typeof createStaticNotebookBackend>,
|
|
39
|
+
cursor?: string,
|
|
40
|
+
) {
|
|
41
|
+
const stream = new NotebookEvents(backend.transport, { cursor });
|
|
42
|
+
streams.push(stream);
|
|
43
|
+
const events: Protocol.EventOutput[] = [];
|
|
44
|
+
await new Promise<void>((resolve, reject) => {
|
|
45
|
+
stream.subscribe((event) => {
|
|
46
|
+
events.push(Protocol.Event.parse(event));
|
|
47
|
+
if (event.event === "ready") {
|
|
48
|
+
resolve();
|
|
49
|
+
}
|
|
50
|
+
}, reject);
|
|
51
|
+
});
|
|
52
|
+
return { stream, events };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("static notebook backend", () => {
|
|
56
|
+
it("supplies a complete document through the normal event reader and reducer", async () => {
|
|
57
|
+
const source = snapshot();
|
|
58
|
+
const { stream, events } = await read(createStaticNotebookBackend(source));
|
|
59
|
+
const reducer = createNotebookReducer({ document: documentResource });
|
|
60
|
+
const state = events.reduce(reducer.reduce, reducer.initial);
|
|
61
|
+
expect(events.map((event) => event.event)).toEqual([
|
|
62
|
+
"notebook",
|
|
63
|
+
"cell",
|
|
64
|
+
"cell",
|
|
65
|
+
"cell",
|
|
66
|
+
"document",
|
|
67
|
+
"ready",
|
|
68
|
+
]);
|
|
69
|
+
expect(state.notebook).toEqual({ ...source.notebook, operations: [] });
|
|
70
|
+
expect(state.resources.document.current).toEqual({
|
|
71
|
+
revision: source.document,
|
|
72
|
+
cells: new Map(source.cells.map((cell) => [cell.id, cell])),
|
|
73
|
+
});
|
|
74
|
+
expect(stream.status).toBe("connected");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("preserves withheld source and its revision metadata", async () => {
|
|
78
|
+
const source = snapshot();
|
|
79
|
+
const cells = source.cells.map((cell) => ({ ...cell, code: null }));
|
|
80
|
+
const { events } = await read(
|
|
81
|
+
createStaticNotebookBackend({ ...source, cells }),
|
|
82
|
+
);
|
|
83
|
+
expect(
|
|
84
|
+
events
|
|
85
|
+
.filter((event) => event.event === "cell")
|
|
86
|
+
.map((event) => event.data.cell),
|
|
87
|
+
).toEqual(cells);
|
|
88
|
+
expect(
|
|
89
|
+
events.find((event) => event.event === "document")?.data.document,
|
|
90
|
+
).toEqual(source.document);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("serves an empty document as available", async () => {
|
|
94
|
+
const source = snapshot();
|
|
95
|
+
const document = { ...source.document, order: [] };
|
|
96
|
+
const { events } = await read(
|
|
97
|
+
createStaticNotebookBackend({ ...source, document, cells: [] }),
|
|
98
|
+
);
|
|
99
|
+
const reducer = createNotebookReducer({ document: documentResource });
|
|
100
|
+
expect(
|
|
101
|
+
events.reduce(reducer.reduce, reducer.initial).resources.document.current,
|
|
102
|
+
).toEqual({
|
|
103
|
+
revision: document,
|
|
104
|
+
cells: new Map(),
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("keeps independent readers open without polling or reconnecting", async () => {
|
|
109
|
+
vi.useFakeTimers();
|
|
110
|
+
const backend = createStaticNotebookBackend(snapshot());
|
|
111
|
+
const first = await read(backend);
|
|
112
|
+
const second = await read(backend);
|
|
113
|
+
first.stream.close();
|
|
114
|
+
await first.stream.closed;
|
|
115
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
116
|
+
expect(second.events).toEqual(first.events);
|
|
117
|
+
expect(second.stream.status).toBe("connected");
|
|
118
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("resets a resume cursor before replaying its snapshot", async () => {
|
|
122
|
+
const { events } = await read(
|
|
123
|
+
createStaticNotebookBackend(snapshot()),
|
|
124
|
+
"previous-10",
|
|
125
|
+
);
|
|
126
|
+
expect(events[0]).toEqual({ event: "reset", data: {} });
|
|
127
|
+
expect(events.at(-1)).toEqual({ event: "ready", id: "static", data: {} });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("advertises no mutations and returns a protocol failure for direct edits", async () => {
|
|
131
|
+
const backend = createStaticNotebookBackend(snapshot());
|
|
132
|
+
expect(await backend.client.edit([])).toEqual({
|
|
133
|
+
ok: false,
|
|
134
|
+
status: 501,
|
|
135
|
+
problem: {
|
|
136
|
+
type: "tag:marimo.io,2026:host/unsupported-operation",
|
|
137
|
+
title: "Unsupported operation",
|
|
138
|
+
status: 501,
|
|
139
|
+
detail: "Static notebooks do not support editing",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("respects cancellation before opening, during delivery, and for commands", async () => {
|
|
145
|
+
const backend = createStaticNotebookBackend(snapshot());
|
|
146
|
+
const before = new AbortController();
|
|
147
|
+
before.abort();
|
|
148
|
+
const open = vi.fn();
|
|
149
|
+
const message = vi.fn();
|
|
150
|
+
await backend.transport(
|
|
151
|
+
{ cursor: null, signal: before.signal },
|
|
152
|
+
{ open, message },
|
|
153
|
+
);
|
|
154
|
+
expect(open).not.toHaveBeenCalled();
|
|
155
|
+
expect(message).not.toHaveBeenCalled();
|
|
156
|
+
await expect(
|
|
157
|
+
backend.client.edit([], { signal: before.signal }),
|
|
158
|
+
).rejects.toBe(before.signal.reason);
|
|
159
|
+
const during = new AbortController();
|
|
160
|
+
message.mockImplementation(() => during.abort());
|
|
161
|
+
await backend.transport(
|
|
162
|
+
{ cursor: null, signal: during.signal },
|
|
163
|
+
{ open, message },
|
|
164
|
+
);
|
|
165
|
+
expect(message).toHaveBeenCalledOnce();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { NotebookClient, NotebookTransport } from "@marimo-team/client";
|
|
4
|
+
|
|
5
|
+
/** Events and commands for the same notebook, independent of React or a replica. */
|
|
6
|
+
export interface NotebookBackend {
|
|
7
|
+
readonly transport: NotebookTransport;
|
|
8
|
+
/** Method presence is not permission; the notebook advertises its operations. */
|
|
9
|
+
readonly client: Pick<NotebookClient, "edit">;
|
|
10
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type * as Protocol from "@marimo-team/client/protocol";
|
|
4
|
+
import type { NotebookBackend } from "./backend";
|
|
5
|
+
|
|
6
|
+
/** A document snapshot with its own identity and revision metadata. */
|
|
7
|
+
export interface StaticNotebookSnapshot {
|
|
8
|
+
readonly notebook: Omit<Protocol.Notebook, "operations">;
|
|
9
|
+
readonly document: Protocol.Document;
|
|
10
|
+
readonly cells: readonly Protocol.Cell[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Serve a document without a server, runtime, or mutation operations.
|
|
15
|
+
* Callers and readers must treat the snapshot records as immutable.
|
|
16
|
+
*/
|
|
17
|
+
export function createStaticNotebookBackend(
|
|
18
|
+
snapshot: StaticNotebookSnapshot,
|
|
19
|
+
): NotebookBackend {
|
|
20
|
+
const frames: Protocol.Event[] = [
|
|
21
|
+
{
|
|
22
|
+
event: "notebook",
|
|
23
|
+
data: { notebook: { ...snapshot.notebook, operations: [] } },
|
|
24
|
+
},
|
|
25
|
+
...snapshot.cells.map((cell): Protocol.CellEvent => ({
|
|
26
|
+
event: "cell",
|
|
27
|
+
data: { cell },
|
|
28
|
+
})),
|
|
29
|
+
{ event: "document", data: { document: snapshot.document } },
|
|
30
|
+
{ event: "ready", id: "static", data: {} },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
transport: ({ cursor, signal }, { open, message }) =>
|
|
35
|
+
new Promise((resolve) => {
|
|
36
|
+
if (signal.aborted) {
|
|
37
|
+
resolve();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Finishing would ask NotebookEvents to reconnect. A snapshot stays open
|
|
41
|
+
// until its reader closes, even though there are no more updates.
|
|
42
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
43
|
+
open();
|
|
44
|
+
if (cursor !== null && !signal.aborted) {
|
|
45
|
+
message({ event: "reset", data: {} });
|
|
46
|
+
}
|
|
47
|
+
for (const frame of frames) {
|
|
48
|
+
if (signal.aborted) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
message(frame);
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
client: {
|
|
55
|
+
edit: async (_changes, options) => {
|
|
56
|
+
options?.signal?.throwIfAborted();
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
status: 501,
|
|
60
|
+
problem: {
|
|
61
|
+
type: "tag:marimo.io,2026:host/unsupported-operation",
|
|
62
|
+
title: "Unsupported operation",
|
|
63
|
+
status: 501,
|
|
64
|
+
detail: "Static notebooks do not support editing",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
5
6
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
6
7
|
import type { CellId } from "@/core/cells/ids";
|
|
7
8
|
import * as requestsModule from "@/core/network/requests";
|
|
@@ -18,7 +19,8 @@ vi.mock("@/utils/copy", () => ({
|
|
|
18
19
|
}));
|
|
19
20
|
|
|
20
21
|
describe("IslandControls", () => {
|
|
21
|
-
const mockSendRun =
|
|
22
|
+
const mockSendRun =
|
|
23
|
+
vi.fn<ReturnType<typeof requestsModule.useRequestClient>["sendRun"]>();
|
|
22
24
|
const mockCopyToClipboard = vi.fn();
|
|
23
25
|
const mockCodeCallback = vi.fn(() => "print('test code')");
|
|
24
26
|
const cellId = "test-cell-id" as CellId;
|
|
@@ -30,11 +32,11 @@ describe("IslandControls", () => {
|
|
|
30
32
|
|
|
31
33
|
beforeEach(() => {
|
|
32
34
|
vi.clearAllMocks();
|
|
33
|
-
mockSendRun.mockResolvedValue(
|
|
35
|
+
mockSendRun.mockResolvedValue(null);
|
|
34
36
|
|
|
35
|
-
vi.spyOn(requestsModule, "useRequestClient").mockReturnValue(
|
|
36
|
-
sendRun: mockSendRun,
|
|
37
|
-
|
|
37
|
+
vi.spyOn(requestsModule, "useRequestClient").mockReturnValue(
|
|
38
|
+
MockRequestClient.create({ sendRun: mockSendRun }),
|
|
39
|
+
);
|
|
38
40
|
|
|
39
41
|
vi.spyOn(copyModule, "copyToClipboard").mockImplementation(
|
|
40
42
|
mockCopyToClipboard,
|