@marimo-team/frontend 0.23.2-dev38 → 0.23.2-dev40
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-CjPI69hZ.js → CellStatus-DVCNDLN4.js} +1 -1
- package/dist/assets/{JsonOutput-CcRaYhT-.js → JsonOutput-CaMHqTRB.js} +1 -1
- package/dist/assets/{MarimoErrorOutput-BYuSTceB.js → MarimoErrorOutput-BlvHXVUx.js} +1 -1
- package/dist/assets/{RenderHTML-DQttWYDe.js → RenderHTML-Cz27UcDX.js} +1 -1
- package/dist/assets/{add-cell-with-ai-CPCm86Mb.js → add-cell-with-ai-5BBZGFp9.js} +1 -1
- package/dist/assets/{add-connection-dialog-dNJgn8Ox.js → add-connection-dialog-BTUO6NJ0.js} +1 -1
- package/dist/assets/{agent-panel-CZV8iRUC.js → agent-panel-D2MAo4Fl.js} +1 -1
- package/dist/assets/{ai-model-dropdown-CHNX-Yvn.js → ai-model-dropdown-DAgirzoG.js} +1 -1
- package/dist/assets/{app-config-button-B7ptjTn8.js → app-config-button-CM7eYfWU.js} +1 -1
- package/dist/assets/{cell-editor-OuSBQcHa.js → cell-editor-CTaekLvG.js} +1 -1
- package/dist/assets/{cell-link-CeI1GPxX.js → cell-link-D8xLFRwF.js} +1 -1
- package/dist/assets/{cells-CkQXmtbU.js → cells-9Zp_JbEx.js} +33 -33
- package/dist/assets/{chat-display-qsryOR04.js → chat-display-C9e79L_T.js} +1 -1
- package/dist/assets/{chat-panel-B0ITjHrb.js → chat-panel-Bd1FEqbW.js} +1 -1
- package/dist/assets/{chat-ui-FgswEfHK.js → chat-ui-DOiOkm2n.js} +1 -1
- package/dist/assets/{column-preview-hHmxmCnB.js → column-preview-CoawaYbp.js} +1 -1
- package/dist/assets/{command-palette-BlNedegf.js → command-palette-BlLPeMPB.js} +1 -1
- package/dist/assets/{common-B1gBVTty.js → common--vhf6hkz.js} +1 -1
- package/dist/assets/{components-GayobceR.js → components-Cdl3TnkR.js} +1 -1
- package/dist/assets/{components-4Llkti3I.js → components-D8goxQ66.js} +1 -1
- package/dist/assets/{datasource-DohSpOc8.js → datasource-AQKUhUjP.js} +1 -1
- package/dist/assets/{dependency-graph-panel-_bVWly6X.js → dependency-graph-panel-Dmzxw1B_.js} +1 -1
- package/dist/assets/{documentation-panel-DBQM75dO.js → documentation-panel-eJQ66av2.js} +1 -1
- package/dist/assets/{download-aayD25Wi.js → download-CsVG4qrR.js} +1 -1
- package/dist/assets/{edit-page-Cw8d_f2S.js → edit-page-C28CPNWD.js} +3 -3
- package/dist/assets/{error-panel-D-9BQN8U.js → error-panel-DMnj0nOD.js} +1 -1
- package/dist/assets/{file-explorer-panel-CxfXG24m.js → file-explorer-panel-DeoEBy9i.js} +1 -1
- package/dist/assets/{file-icons-UkZeY45t.js → file-icons-DLa7-MLk.js} +1 -1
- package/dist/assets/{floating-outline--H6knKWf.js → floating-outline-DdtzicWh.js} +1 -1
- package/dist/assets/{focus-DxuOmtKI.js → focus-COBUb5b6.js} +1 -1
- package/dist/assets/{form-3fPAWHbK.js → form-BNhPt7_q.js} +1 -1
- package/dist/assets/{home-page-_beA3R51.js → home-page-BbQwtlN0.js} +1 -1
- package/dist/assets/{hooks-B4ys0c0n.js → hooks-XpLDm4ny.js} +1 -1
- package/dist/assets/{html-to-image-RqGk5tNG.js → html-to-image-BB3CKaMw.js} +1 -1
- package/dist/assets/{index-lpZP7WFc.js → index-ddxYLCBl.js} +4 -4
- package/dist/assets/{kiosk-mode-CtVLUXra.js → kiosk-mode-BVPZu333.js} +1 -1
- package/dist/assets/{layout-Dz6l5gDf.js → layout-CU55YmC3.js} +1 -1
- package/dist/assets/{logs-panel-Gg9rCqvY.js → logs-panel-C8usBneO.js} +1 -1
- package/dist/assets/{markdown-renderer-CPtNggG4.js → markdown-renderer-BclMAJo1.js} +1 -1
- package/dist/assets/{name-cell-input-D3CxUPEc.js → name-cell-input-B_6HAmm5.js} +1 -1
- package/dist/assets/{outline-panel-COUl_K0O.js → outline-panel-ljJ_fuK8.js} +1 -1
- package/dist/assets/{packages-panel-C_MusfCT.js → packages-panel-4kAgcZnP.js} +1 -1
- package/dist/assets/{panels-DwDu5SGI.js → panels-CHFFNaFY.js} +1 -1
- package/dist/assets/{process-output-B4IfPdhN.js → process-output-GJTUEkWa.js} +1 -1
- package/dist/assets/{readonly-python-code-CcyUzrM-.js → readonly-python-code-Lck67mJo.js} +1 -1
- package/dist/assets/{run-page-C2kQLw9h.js → run-page-Tt0WlqGQ.js} +1 -1
- package/dist/assets/{scratchpad-panel-BN9Sv2Hh.js → scratchpad-panel-CEN-dBEu.js} +1 -1
- package/dist/assets/{session-panel-CMI4faW3.js → session-panel-CcYW5DIV.js} +1 -1
- package/dist/assets/{snippets-panel-lJwSEA5M.js → snippets-panel-B87fFN0r.js} +1 -1
- package/dist/assets/state-BV4n_RxL.js +3 -0
- package/dist/assets/{state-DbB3Bx1n.js → state-DF3-mDqB.js} +1 -1
- package/dist/assets/{textarea-jLEyGvlo.js → textarea-DwVik6yG.js} +1 -1
- package/dist/assets/{tracing-CFor4yuK.js → tracing-BzXQ5H6_.js} +1 -1
- package/dist/assets/{tracing-panel-DM9aAZvv.js → tracing-panel-DyqA4WqJ.js} +2 -2
- package/dist/assets/{useAddCell-pxYjm9Vz.js → useAddCell-DgomfDJt.js} +1 -1
- package/dist/assets/{useCellActionButton-GNljpGS_.js → useCellActionButton-CfZZmpoB.js} +1 -1
- package/dist/assets/{useDeleteCell-1EiCOF19.js → useDeleteCell-DNcoM-0z.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-DLosV83A.js → useDependencyPanelTab-eVqw00QY.js} +1 -1
- package/dist/assets/{useNotebookActions-D_eNWvNH.js → useNotebookActions-Di_CkPfK.js} +1 -1
- package/dist/assets/{useRunCells-BGxIfw3R.js → useRunCells-DM6jetbr.js} +1 -1
- package/dist/assets/{useSplitCell-oguWPZqA.js → useSplitCell-BDm4rmCg.js} +1 -1
- package/dist/index.html +22 -22
- package/package.json +1 -1
- package/src/core/cells/__tests__/apply-transaction.test.ts +441 -0
- package/src/core/cells/__tests__/cells.test.ts +110 -0
- package/src/core/cells/cells.ts +18 -0
- package/src/core/cells/document-changes.ts +34 -1
- package/src/plugins/core/__test__/trusted-url.test.ts +45 -1
- package/src/plugins/core/trusted-url.ts +27 -2
- package/dist/assets/state-BoK4XWzN.js +0 -3
|
@@ -106,6 +106,14 @@ function pretty(s: NotebookState): string {
|
|
|
106
106
|
return `\n${lines.join("\n")}\n`;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/** Snapshot showing the physical column grouping in the MultiColumn tree. */
|
|
110
|
+
function prettyColumns(s: NotebookState): string {
|
|
111
|
+
const lines = s.cellIds
|
|
112
|
+
.getColumns()
|
|
113
|
+
.map((col, idx) => `col${idx}: [${col.inOrderIds.join(", ")}]`);
|
|
114
|
+
return `\n${lines.join("\n")}\n`;
|
|
115
|
+
}
|
|
116
|
+
|
|
109
117
|
let i = 0;
|
|
110
118
|
|
|
111
119
|
beforeAll(() => {
|
|
@@ -134,6 +142,14 @@ describe("applyTransactionChanges edge cases", () => {
|
|
|
134
142
|
new-cell: 'configured' [hide_code, disabled, col=1]
|
|
135
143
|
"
|
|
136
144
|
`);
|
|
145
|
+
// The new cell must physically land in the second column, not just
|
|
146
|
+
// carry col=1 as stale metadata.
|
|
147
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
148
|
+
"
|
|
149
|
+
col0: [0]
|
|
150
|
+
col1: [new-cell]
|
|
151
|
+
"
|
|
152
|
+
`);
|
|
137
153
|
});
|
|
138
154
|
|
|
139
155
|
it("create-cell then move-cell in same transaction", () => {
|
|
@@ -348,3 +364,428 @@ describe("applyTransactionChanges edge cases", () => {
|
|
|
348
364
|
expect(editorView?.state.doc.toString()).toBe('x = "AFTER"');
|
|
349
365
|
});
|
|
350
366
|
});
|
|
367
|
+
|
|
368
|
+
describe("applyTransactionChanges column rebuild", () => {
|
|
369
|
+
it("boundary anchors: set-config on column boundaries splits cells into columns", () => {
|
|
370
|
+
// The user's exact example. Server sends a reorder + set-config only on
|
|
371
|
+
// the cells at column boundaries. The replica must infer that the cells
|
|
372
|
+
// in between inherit the column of the preceding anchor.
|
|
373
|
+
setup("a", "b", "c", "d");
|
|
374
|
+
const [a, b, c, d] = state.cellIds.inOrderIds;
|
|
375
|
+
apply([
|
|
376
|
+
{ type: "reorder-cells", cellIds: [a, b, c, d] },
|
|
377
|
+
{
|
|
378
|
+
type: "set-config",
|
|
379
|
+
cellId: a,
|
|
380
|
+
column: 0,
|
|
381
|
+
disabled: false,
|
|
382
|
+
hideCode: false,
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
type: "set-config",
|
|
386
|
+
cellId: c,
|
|
387
|
+
column: 1,
|
|
388
|
+
disabled: false,
|
|
389
|
+
hideCode: false,
|
|
390
|
+
},
|
|
391
|
+
]);
|
|
392
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
393
|
+
"
|
|
394
|
+
col0: [0, 1]
|
|
395
|
+
col1: [2, 3]
|
|
396
|
+
"
|
|
397
|
+
`);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("boundary anchors on reordered cells: order follows reorder-cells", () => {
|
|
401
|
+
// Start from a single column, reorder the cells and split at c.
|
|
402
|
+
// The rebuild must use the new flat order from reorder-cells so that
|
|
403
|
+
// b ends up next to a (col 0) and d ends up next to c (col 1).
|
|
404
|
+
setup("a", "b", "c", "d");
|
|
405
|
+
const [a, b, c, d] = state.cellIds.inOrderIds;
|
|
406
|
+
apply([
|
|
407
|
+
{ type: "reorder-cells", cellIds: [d, a, c, b] },
|
|
408
|
+
{
|
|
409
|
+
type: "set-config",
|
|
410
|
+
cellId: d,
|
|
411
|
+
column: 0,
|
|
412
|
+
disabled: false,
|
|
413
|
+
hideCode: false,
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
type: "set-config",
|
|
417
|
+
cellId: c,
|
|
418
|
+
column: 1,
|
|
419
|
+
disabled: false,
|
|
420
|
+
hideCode: false,
|
|
421
|
+
},
|
|
422
|
+
]);
|
|
423
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
424
|
+
"
|
|
425
|
+
col0: [3, 0]
|
|
426
|
+
col1: [2, 1]
|
|
427
|
+
"
|
|
428
|
+
`);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("three columns: inherits column through consecutive cells", () => {
|
|
432
|
+
setup("a", "b", "c", "d", "e", "f");
|
|
433
|
+
const [a, b, c, d, e, f] = state.cellIds.inOrderIds;
|
|
434
|
+
apply([
|
|
435
|
+
{ type: "reorder-cells", cellIds: [a, b, c, d, e, f] },
|
|
436
|
+
{
|
|
437
|
+
type: "set-config",
|
|
438
|
+
cellId: a,
|
|
439
|
+
column: 0,
|
|
440
|
+
disabled: false,
|
|
441
|
+
hideCode: false,
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
type: "set-config",
|
|
445
|
+
cellId: c,
|
|
446
|
+
column: 1,
|
|
447
|
+
disabled: false,
|
|
448
|
+
hideCode: false,
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
type: "set-config",
|
|
452
|
+
cellId: e,
|
|
453
|
+
column: 2,
|
|
454
|
+
disabled: false,
|
|
455
|
+
hideCode: false,
|
|
456
|
+
},
|
|
457
|
+
]);
|
|
458
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
459
|
+
"
|
|
460
|
+
col0: [0, 1]
|
|
461
|
+
col1: [2, 3]
|
|
462
|
+
col2: [4, 5]
|
|
463
|
+
"
|
|
464
|
+
`);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("every cell explicitly tagged with a column", () => {
|
|
468
|
+
setup("a", "b", "c", "d");
|
|
469
|
+
const [a, b, c, d] = state.cellIds.inOrderIds;
|
|
470
|
+
apply([
|
|
471
|
+
{ type: "reorder-cells", cellIds: [a, b, c, d] },
|
|
472
|
+
{ type: "set-config", cellId: a, column: 0 },
|
|
473
|
+
{ type: "set-config", cellId: b, column: 1 },
|
|
474
|
+
{ type: "set-config", cellId: c, column: 0 },
|
|
475
|
+
{ type: "set-config", cellId: d, column: 1 },
|
|
476
|
+
]);
|
|
477
|
+
// a and c in col0; b and d in col1. Order within each column follows
|
|
478
|
+
// the reorder-cells order.
|
|
479
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
480
|
+
"
|
|
481
|
+
col0: [0, 2]
|
|
482
|
+
col1: [1, 3]
|
|
483
|
+
"
|
|
484
|
+
`);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("set-config without reorder-cells moves the cell to the new column", () => {
|
|
488
|
+
setup("a", "b", "c");
|
|
489
|
+
const [, b] = state.cellIds.inOrderIds;
|
|
490
|
+
apply([{ type: "set-config", cellId: b, column: 1 }]);
|
|
491
|
+
// Without reorder-cells, the flat order comes from the current tree.
|
|
492
|
+
// b gets explicit col=1; a and c stay with default null → follow the
|
|
493
|
+
// previous cell's column (a → col0 because prev=0; c → col1 because
|
|
494
|
+
// prev was just set to 1 by b).
|
|
495
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
496
|
+
"
|
|
497
|
+
col0: [0]
|
|
498
|
+
col1: [1, 2]
|
|
499
|
+
"
|
|
500
|
+
`);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("no column change: set-config only touching other fields does not repartition", () => {
|
|
504
|
+
setup("a", "b", "c");
|
|
505
|
+
const [, b] = state.cellIds.inOrderIds;
|
|
506
|
+
apply([{ type: "set-config", cellId: b, hideCode: true }]);
|
|
507
|
+
// All three cells remain in a single column. No rebuild triggered.
|
|
508
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
509
|
+
"
|
|
510
|
+
col0: [0, 1, 2]
|
|
511
|
+
"
|
|
512
|
+
`);
|
|
513
|
+
expect(pretty(state)).toMatchInlineSnapshot(`
|
|
514
|
+
"
|
|
515
|
+
0: 'a'
|
|
516
|
+
1: 'b' [hide_code]
|
|
517
|
+
2: 'c'
|
|
518
|
+
"
|
|
519
|
+
`);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("no changes: empty transaction leaves column structure alone", () => {
|
|
523
|
+
// Setup a multi-column state first
|
|
524
|
+
setup("a", "b");
|
|
525
|
+
const [a, b] = state.cellIds.inOrderIds;
|
|
526
|
+
apply([
|
|
527
|
+
{ type: "reorder-cells", cellIds: [a, b] },
|
|
528
|
+
{ type: "set-config", cellId: a, column: 0 },
|
|
529
|
+
{ type: "set-config", cellId: b, column: 1 },
|
|
530
|
+
]);
|
|
531
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
532
|
+
"
|
|
533
|
+
col0: [0]
|
|
534
|
+
col1: [1]
|
|
535
|
+
"
|
|
536
|
+
`);
|
|
537
|
+
// Now apply no changes — column structure should be preserved.
|
|
538
|
+
apply([]);
|
|
539
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
540
|
+
"
|
|
541
|
+
col0: [0]
|
|
542
|
+
col1: [1]
|
|
543
|
+
"
|
|
544
|
+
`);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("merging columns: set-config col=0 for everything collapses to one column", () => {
|
|
548
|
+
// Start in a multi-column state.
|
|
549
|
+
setup("a", "b", "c", "d");
|
|
550
|
+
const [a, b, c, d] = state.cellIds.inOrderIds;
|
|
551
|
+
apply([
|
|
552
|
+
{ type: "reorder-cells", cellIds: [a, b, c, d] },
|
|
553
|
+
{ type: "set-config", cellId: a, column: 0 },
|
|
554
|
+
{ type: "set-config", cellId: c, column: 1 },
|
|
555
|
+
]);
|
|
556
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
557
|
+
"
|
|
558
|
+
col0: [0, 1]
|
|
559
|
+
col1: [2, 3]
|
|
560
|
+
"
|
|
561
|
+
`);
|
|
562
|
+
// Now merge everything back to col 0.
|
|
563
|
+
apply([
|
|
564
|
+
{ type: "reorder-cells", cellIds: [a, b, c, d] },
|
|
565
|
+
{ type: "set-config", cellId: c, column: 0 },
|
|
566
|
+
{ type: "set-config", cellId: d, column: 0 },
|
|
567
|
+
]);
|
|
568
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
569
|
+
"
|
|
570
|
+
col0: [0, 1, 2, 3]
|
|
571
|
+
"
|
|
572
|
+
`);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("create-cell with column places cell in correct column", () => {
|
|
576
|
+
setup("a", "b");
|
|
577
|
+
const [, b] = state.cellIds.inOrderIds;
|
|
578
|
+
apply([
|
|
579
|
+
{
|
|
580
|
+
type: "create-cell",
|
|
581
|
+
cellId: cellId("fresh"),
|
|
582
|
+
code: "x",
|
|
583
|
+
name: "",
|
|
584
|
+
config: { column: 1 },
|
|
585
|
+
after: b,
|
|
586
|
+
},
|
|
587
|
+
]);
|
|
588
|
+
// a and b are in col 0 (unchanged). The new cell is created at the end
|
|
589
|
+
// with column=1, so it should land physically in col 1.
|
|
590
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
591
|
+
"
|
|
592
|
+
col0: [0, 1]
|
|
593
|
+
col1: [fresh]
|
|
594
|
+
"
|
|
595
|
+
`);
|
|
596
|
+
expect(pretty(state)).toMatchInlineSnapshot(`
|
|
597
|
+
"
|
|
598
|
+
0: 'a'
|
|
599
|
+
1: 'b'
|
|
600
|
+
fresh: 'x' [col=1]
|
|
601
|
+
"
|
|
602
|
+
`);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it("multi-change transaction with column + code + name updates", () => {
|
|
606
|
+
setup("a", "b", "c");
|
|
607
|
+
const [a, b, c] = state.cellIds.inOrderIds;
|
|
608
|
+
apply([
|
|
609
|
+
{ type: "reorder-cells", cellIds: [a, b, c] },
|
|
610
|
+
{ type: "set-code", cellId: a, code: "x = 1" },
|
|
611
|
+
{ type: "set-name", cellId: b, name: "middle" },
|
|
612
|
+
{ type: "set-config", cellId: a, column: 0 },
|
|
613
|
+
{ type: "set-config", cellId: b, column: 1 },
|
|
614
|
+
]);
|
|
615
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
616
|
+
"
|
|
617
|
+
col0: [0]
|
|
618
|
+
col1: [1, 2]
|
|
619
|
+
"
|
|
620
|
+
`);
|
|
621
|
+
expect(pretty(state)).toMatchInlineSnapshot(`
|
|
622
|
+
"
|
|
623
|
+
0: 'x = 1' [col=0]
|
|
624
|
+
1: 'b' [name=middle, col=1]
|
|
625
|
+
2: 'c'
|
|
626
|
+
"
|
|
627
|
+
`);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it("cancelled create+delete does not trigger column rebuild", () => {
|
|
631
|
+
setup("a", "b");
|
|
632
|
+
apply([
|
|
633
|
+
{
|
|
634
|
+
type: "create-cell",
|
|
635
|
+
cellId: cellId("ephemeral"),
|
|
636
|
+
code: "tmp",
|
|
637
|
+
name: "",
|
|
638
|
+
config: { column: 1 },
|
|
639
|
+
},
|
|
640
|
+
{ type: "delete-cell", cellId: cellId("ephemeral") },
|
|
641
|
+
]);
|
|
642
|
+
// The create+delete cancel out. The column metadata on the cancelled
|
|
643
|
+
// create-cell shouldn't cause a spurious column rebuild.
|
|
644
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
645
|
+
"
|
|
646
|
+
col0: [0, 1]
|
|
647
|
+
"
|
|
648
|
+
`);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it("boundary-anchor convention: moving an anchor pulls non-anchored followers", () => {
|
|
652
|
+
// This documents the boundary-anchor convention used by the server: only
|
|
653
|
+
// cells at column boundaries carry an explicit column. Non-anchor cells
|
|
654
|
+
// have config.column=null and inherit the column of the previous cell.
|
|
655
|
+
// That means moving an anchor also moves its silent followers.
|
|
656
|
+
setup("a", "b", "c", "d");
|
|
657
|
+
const [a, b, c, d] = state.cellIds.inOrderIds;
|
|
658
|
+
// Split into two columns. Only a and c are anchors.
|
|
659
|
+
apply([
|
|
660
|
+
{ type: "reorder-cells", cellIds: [a, b, c, d] },
|
|
661
|
+
{ type: "set-config", cellId: a, column: 0 },
|
|
662
|
+
{ type: "set-config", cellId: c, column: 1 },
|
|
663
|
+
]);
|
|
664
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
665
|
+
"
|
|
666
|
+
col0: [0, 1]
|
|
667
|
+
col1: [2, 3]
|
|
668
|
+
"
|
|
669
|
+
`);
|
|
670
|
+
// Move the c anchor to col 0. d has no explicit column so it follows c.
|
|
671
|
+
apply([
|
|
672
|
+
{ type: "reorder-cells", cellIds: [a, b, c, d] },
|
|
673
|
+
{ type: "set-config", cellId: c, column: 0 },
|
|
674
|
+
]);
|
|
675
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
676
|
+
"
|
|
677
|
+
col0: [0, 1, 2, 3]
|
|
678
|
+
"
|
|
679
|
+
`);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it("explicit anchors on followers: move anchor leaves explicitly-tagged follower behind", () => {
|
|
683
|
+
// Contrast with the previous test: if d is explicitly tagged col=1,
|
|
684
|
+
// moving c back to col 0 should leave d in col 1.
|
|
685
|
+
setup("a", "b", "c", "d");
|
|
686
|
+
const [a, b, c, d] = state.cellIds.inOrderIds;
|
|
687
|
+
apply([
|
|
688
|
+
{ type: "reorder-cells", cellIds: [a, b, c, d] },
|
|
689
|
+
{ type: "set-config", cellId: a, column: 0 },
|
|
690
|
+
{ type: "set-config", cellId: c, column: 1 },
|
|
691
|
+
{ type: "set-config", cellId: d, column: 1 },
|
|
692
|
+
]);
|
|
693
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
694
|
+
"
|
|
695
|
+
col0: [0, 1]
|
|
696
|
+
col1: [2, 3]
|
|
697
|
+
"
|
|
698
|
+
`);
|
|
699
|
+
// Move c back to col 0. Because d has its own explicit column=1, it
|
|
700
|
+
// does NOT follow c.
|
|
701
|
+
apply([
|
|
702
|
+
{ type: "reorder-cells", cellIds: [a, b, c, d] },
|
|
703
|
+
{ type: "set-config", cellId: c, column: 0 },
|
|
704
|
+
]);
|
|
705
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
706
|
+
"
|
|
707
|
+
col0: [0, 1, 2]
|
|
708
|
+
col1: [3]
|
|
709
|
+
"
|
|
710
|
+
`);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it("set-config before reorder-cells: processing is sorted so set-config runs last", () => {
|
|
714
|
+
// Defensive test: even if a transaction has set-config *before*
|
|
715
|
+
// reorder-cells, the implementation must process reorder-cells first so
|
|
716
|
+
// that the column rebuild sees the intended final flat order. This
|
|
717
|
+
// matches the order the backend plans transactions in.
|
|
718
|
+
setup("a", "b", "c", "d");
|
|
719
|
+
const [a, b, c, d] = state.cellIds.inOrderIds;
|
|
720
|
+
apply([
|
|
721
|
+
// set-config appears FIRST in the transaction.
|
|
722
|
+
{ type: "set-config", cellId: a, column: 0 },
|
|
723
|
+
{ type: "set-config", cellId: c, column: 1 },
|
|
724
|
+
// reorder-cells comes afterwards.
|
|
725
|
+
{ type: "reorder-cells", cellIds: [a, b, c, d] },
|
|
726
|
+
]);
|
|
727
|
+
// Result must be identical to the canonical order:
|
|
728
|
+
// the tree is reshaped first, then column metadata is applied.
|
|
729
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
730
|
+
"
|
|
731
|
+
col0: [0, 1]
|
|
732
|
+
col1: [2, 3]
|
|
733
|
+
"
|
|
734
|
+
`);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it("set-config interleaved between other changes is still run last", () => {
|
|
738
|
+
// Another ordering variant: set-config interleaved between set-code and
|
|
739
|
+
// reorder-cells. Our sort must move set-config to the end regardless of
|
|
740
|
+
// position, while preserving the relative order of the other changes.
|
|
741
|
+
setup("a", "b", "c", "d");
|
|
742
|
+
const [a, b, c, d] = state.cellIds.inOrderIds;
|
|
743
|
+
apply([
|
|
744
|
+
{ type: "set-config", cellId: a, column: 0 },
|
|
745
|
+
{ type: "set-code", cellId: a, code: "x = 1" },
|
|
746
|
+
{ type: "set-config", cellId: c, column: 1 },
|
|
747
|
+
{ type: "reorder-cells", cellIds: [a, b, c, d] },
|
|
748
|
+
{ type: "set-name", cellId: d, name: "last" },
|
|
749
|
+
]);
|
|
750
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
751
|
+
"
|
|
752
|
+
col0: [0, 1]
|
|
753
|
+
col1: [2, 3]
|
|
754
|
+
"
|
|
755
|
+
`);
|
|
756
|
+
expect(pretty(state)).toMatchInlineSnapshot(`
|
|
757
|
+
"
|
|
758
|
+
0: 'x = 1' [col=0]
|
|
759
|
+
1: 'b'
|
|
760
|
+
2: 'c' [col=1]
|
|
761
|
+
3: 'd' [name=last]
|
|
762
|
+
"
|
|
763
|
+
`);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("reorder-cells alone (no column changes) preserves existing columns", () => {
|
|
767
|
+
// Start in a two-column state.
|
|
768
|
+
setup("a", "b", "c", "d");
|
|
769
|
+
const [a, b, c, d] = state.cellIds.inOrderIds;
|
|
770
|
+
apply([
|
|
771
|
+
{ type: "reorder-cells", cellIds: [a, b, c, d] },
|
|
772
|
+
{ type: "set-config", cellId: a, column: 0 },
|
|
773
|
+
{ type: "set-config", cellId: c, column: 1 },
|
|
774
|
+
]);
|
|
775
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
776
|
+
"
|
|
777
|
+
col0: [0, 1]
|
|
778
|
+
col1: [2, 3]
|
|
779
|
+
"
|
|
780
|
+
`);
|
|
781
|
+
// Now reorder without any column changes. The rebuild must NOT fire.
|
|
782
|
+
// setCellIds with fromWithPreviousShape preserves the column assignments.
|
|
783
|
+
apply([{ type: "reorder-cells", cellIds: [b, a, d, c] }]);
|
|
784
|
+
expect(prettyColumns(state)).toMatchInlineSnapshot(`
|
|
785
|
+
"
|
|
786
|
+
col0: [1, 0]
|
|
787
|
+
col1: [3, 2]
|
|
788
|
+
"
|
|
789
|
+
`);
|
|
790
|
+
});
|
|
791
|
+
});
|
|
@@ -2206,6 +2206,116 @@ describe("cell reducer", () => {
|
|
|
2206
2206
|
expect(state.cellIds.getColumns()[2].topLevelIds).toEqual([cellId("2")]);
|
|
2207
2207
|
});
|
|
2208
2208
|
|
|
2209
|
+
it("rebuildCellColumns regroups cells by config.column", () => {
|
|
2210
|
+
// Create four cells in a single column.
|
|
2211
|
+
actions.createNewCell({ cellId: firstCellId, before: false });
|
|
2212
|
+
actions.createNewCell({ cellId: cellId("1"), before: false });
|
|
2213
|
+
actions.createNewCell({ cellId: cellId("2"), before: false });
|
|
2214
|
+
expect(state.cellIds.getColumns().length).toBe(1);
|
|
2215
|
+
|
|
2216
|
+
// Explicitly set config.column on each cell. This ONLY updates metadata —
|
|
2217
|
+
// updateCellConfig does not touch the MultiColumn tree.
|
|
2218
|
+
actions.updateCellConfig({
|
|
2219
|
+
cellId: cellId("0"),
|
|
2220
|
+
config: { column: 0 },
|
|
2221
|
+
});
|
|
2222
|
+
actions.updateCellConfig({
|
|
2223
|
+
cellId: cellId("2"),
|
|
2224
|
+
config: { column: 1 },
|
|
2225
|
+
});
|
|
2226
|
+
|
|
2227
|
+
// Tree is still a single column at this point.
|
|
2228
|
+
expect(state.cellIds.getColumns().length).toBe(1);
|
|
2229
|
+
|
|
2230
|
+
// Now rebuild the column tree from metadata.
|
|
2231
|
+
actions.rebuildCellColumns({
|
|
2232
|
+
cellIds: [cellId("0"), cellId("1"), cellId("2"), cellId("3")],
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
expect(state.cellIds.getColumns().length).toBe(2);
|
|
2236
|
+
// Cell 1 inherits from 0 (col 0), cell 3 inherits from 2 (col 1).
|
|
2237
|
+
expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
|
|
2238
|
+
cellId("0"),
|
|
2239
|
+
cellId("1"),
|
|
2240
|
+
]);
|
|
2241
|
+
expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
|
|
2242
|
+
cellId("2"),
|
|
2243
|
+
cellId("3"),
|
|
2244
|
+
]);
|
|
2245
|
+
});
|
|
2246
|
+
|
|
2247
|
+
it("rebuildCellColumns with explicit column on every cell", () => {
|
|
2248
|
+
actions.createNewCell({ cellId: firstCellId, before: false });
|
|
2249
|
+
actions.createNewCell({ cellId: cellId("1"), before: false });
|
|
2250
|
+
actions.createNewCell({ cellId: cellId("2"), before: false });
|
|
2251
|
+
|
|
2252
|
+
for (const [id, col] of [
|
|
2253
|
+
["0", 1],
|
|
2254
|
+
["1", 0],
|
|
2255
|
+
["2", 1],
|
|
2256
|
+
["3", 0],
|
|
2257
|
+
] as const) {
|
|
2258
|
+
actions.updateCellConfig({ cellId: cellId(id), config: { column: col } });
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
actions.rebuildCellColumns({
|
|
2262
|
+
cellIds: [cellId("0"), cellId("1"), cellId("2"), cellId("3")],
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
expect(state.cellIds.getColumns().length).toBe(2);
|
|
2266
|
+
expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
|
|
2267
|
+
cellId("1"),
|
|
2268
|
+
cellId("3"),
|
|
2269
|
+
]);
|
|
2270
|
+
expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
|
|
2271
|
+
cellId("0"),
|
|
2272
|
+
cellId("2"),
|
|
2273
|
+
]);
|
|
2274
|
+
});
|
|
2275
|
+
|
|
2276
|
+
it("rebuildCellColumns collapses to one column when all cells are col 0", () => {
|
|
2277
|
+
actions.createNewCell({ cellId: firstCellId, before: false });
|
|
2278
|
+
actions.addColumnBreakpoint({ cellId: cellId("1") });
|
|
2279
|
+
expect(state.cellIds.getColumns().length).toBe(2);
|
|
2280
|
+
|
|
2281
|
+
// Wipe column metadata back to 0 for both cells.
|
|
2282
|
+
actions.updateCellConfig({ cellId: cellId("0"), config: { column: 0 } });
|
|
2283
|
+
actions.updateCellConfig({ cellId: cellId("1"), config: { column: 0 } });
|
|
2284
|
+
|
|
2285
|
+
actions.rebuildCellColumns({
|
|
2286
|
+
cellIds: [cellId("0"), cellId("1")],
|
|
2287
|
+
});
|
|
2288
|
+
|
|
2289
|
+
expect(state.cellIds.getColumns().length).toBe(1);
|
|
2290
|
+
expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([
|
|
2291
|
+
cellId("0"),
|
|
2292
|
+
cellId("1"),
|
|
2293
|
+
]);
|
|
2294
|
+
});
|
|
2295
|
+
|
|
2296
|
+
it("rebuildCellColumns follows the provided order, not current tree order", () => {
|
|
2297
|
+
// Start with a single column.
|
|
2298
|
+
actions.createNewCell({ cellId: firstCellId, before: false });
|
|
2299
|
+
actions.createNewCell({ cellId: cellId("1"), before: false });
|
|
2300
|
+
actions.updateCellConfig({ cellId: cellId("0"), config: { column: 0 } });
|
|
2301
|
+
actions.updateCellConfig({ cellId: cellId("2"), config: { column: 1 } });
|
|
2302
|
+
|
|
2303
|
+
// Pass the reversed order. The rebuild should honor it.
|
|
2304
|
+
actions.rebuildCellColumns({
|
|
2305
|
+
cellIds: [cellId("2"), cellId("1"), cellId("0")],
|
|
2306
|
+
});
|
|
2307
|
+
|
|
2308
|
+
// cellIds iteration:
|
|
2309
|
+
// 2 → col=1, pushed to col1, prev=1
|
|
2310
|
+
// 1 → col=null, pushed to col[prev=1] = col1
|
|
2311
|
+
// 0 → col=0, pushed to col0
|
|
2312
|
+
expect(state.cellIds.getColumns()[0].topLevelIds).toEqual([cellId("0")]);
|
|
2313
|
+
expect(state.cellIds.getColumns()[1].topLevelIds).toEqual([
|
|
2314
|
+
cellId("2"),
|
|
2315
|
+
cellId("1"),
|
|
2316
|
+
]);
|
|
2317
|
+
});
|
|
2318
|
+
|
|
2209
2319
|
it("can clear output of a single cell", () => {
|
|
2210
2320
|
// Set up initial state with output
|
|
2211
2321
|
actions.handleCellMessage({
|
package/src/core/cells/cells.ts
CHANGED
|
@@ -874,6 +874,24 @@ const {
|
|
|
874
874
|
cellHandles: nextCellHandles,
|
|
875
875
|
};
|
|
876
876
|
},
|
|
877
|
+
/**
|
|
878
|
+
* Rebuild the MultiColumn tree using each cell's `config.column` value.
|
|
879
|
+
*
|
|
880
|
+
* Used after a transaction whose `set-config` changes updated cells'
|
|
881
|
+
* column metadata without physically moving them in the tree. Cells with
|
|
882
|
+
* `config.column == null` inherit the column of the previous cell in the
|
|
883
|
+
* given order (see `MultiColumn.fromIdsAndColumns`), which lets the server
|
|
884
|
+
* send column changes only at column boundaries.
|
|
885
|
+
*/
|
|
886
|
+
rebuildCellColumns: (state, action: { cellIds: CellId[] }) => {
|
|
887
|
+
const newCellIds = MultiColumn.fromIdsAndColumns(
|
|
888
|
+
action.cellIds.map((id) => [
|
|
889
|
+
id,
|
|
890
|
+
state.cellData[id]?.config.column ?? null,
|
|
891
|
+
]),
|
|
892
|
+
);
|
|
893
|
+
return { ...state, cellIds: newCellIds };
|
|
894
|
+
},
|
|
877
895
|
setCellCodes: (
|
|
878
896
|
state,
|
|
879
897
|
action: {
|
|
@@ -349,6 +349,7 @@ export function toDocumentChanges(
|
|
|
349
349
|
case "prepareForRun":
|
|
350
350
|
case "handleCellMessage":
|
|
351
351
|
case "setCellIds":
|
|
352
|
+
case "rebuildCellColumns":
|
|
352
353
|
case "setCellCodes":
|
|
353
354
|
case "setCells":
|
|
354
355
|
case "setStdinResponse":
|
|
@@ -624,7 +625,24 @@ export function applyTransactionChanges(
|
|
|
624
625
|
): void {
|
|
625
626
|
const cancelled = cancelledCellIds(changes);
|
|
626
627
|
|
|
627
|
-
|
|
628
|
+
// Process set-config changes after everything else. The tree must be fully
|
|
629
|
+
// restructured (create-cell, delete-cell, reorder-cells, move-cell) before
|
|
630
|
+
// we start applying column metadata, since the follow-up rebuildCellColumns
|
|
631
|
+
// step interprets each cell's config.column against the *final* flat order.
|
|
632
|
+
// Sorting is stable within each group.
|
|
633
|
+
const sortedChanges: TransactionChange[] = [
|
|
634
|
+
...changes.filter((c) => c.type !== "set-config"),
|
|
635
|
+
...changes.filter((c) => c.type === "set-config"),
|
|
636
|
+
];
|
|
637
|
+
|
|
638
|
+
// Track whether any change updated a cell's column, and remember the final
|
|
639
|
+
// flat order produced by a reorder-cells change (if any). After all changes
|
|
640
|
+
// are applied, these are used to rebuild the MultiColumn tree so that cells
|
|
641
|
+
// physically move to the column their metadata says they belong in.
|
|
642
|
+
let hasColumnChange = false;
|
|
643
|
+
let reorderOrder: CellId[] | null = null;
|
|
644
|
+
|
|
645
|
+
for (const change of sortedChanges) {
|
|
628
646
|
if (
|
|
629
647
|
cancelled.size > 0 &&
|
|
630
648
|
"cellId" in change &&
|
|
@@ -632,11 +650,26 @@ export function applyTransactionChanges(
|
|
|
632
650
|
) {
|
|
633
651
|
continue;
|
|
634
652
|
}
|
|
653
|
+
if (change.type === "set-config" && change.column != null) {
|
|
654
|
+
hasColumnChange = true;
|
|
655
|
+
}
|
|
656
|
+
if (change.type === "create-cell" && change.config?.column != null) {
|
|
657
|
+
hasColumnChange = true;
|
|
658
|
+
}
|
|
659
|
+
if (change.type === "reorder-cells") {
|
|
660
|
+
reorderOrder = change.cellIds as CellId[];
|
|
661
|
+
}
|
|
635
662
|
for (const action of fromDocumentChanges([change], getCurrentCellIds)) {
|
|
636
663
|
// @ts-expect-error - TypeScript is not smart enough to know we have correctly mapped type -> payload
|
|
637
664
|
actions[action.type](action.payload);
|
|
638
665
|
}
|
|
639
666
|
}
|
|
667
|
+
|
|
668
|
+
if (hasColumnChange) {
|
|
669
|
+
actions.rebuildCellColumns({
|
|
670
|
+
cellIds: reorderOrder ?? getCurrentCellIds(),
|
|
671
|
+
});
|
|
672
|
+
}
|
|
640
673
|
}
|
|
641
674
|
|
|
642
675
|
// ---------------------------------------------------------------------------
|