@marimo-team/islands 0.21.2-dev36 → 0.21.2-dev39

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/main.js CHANGED
@@ -27014,6 +27014,7 @@ ${c.sqlString}
27014
27014
  function indentOneTab(e) {
27015
27015
  return e.split("\n").map((e2) => (e2 == null ? void 0 : e2.trim()) ? ` ${e2}` : e2).join("\n");
27016
27016
  }
27017
+ const loroSyncAnnotation = Annotation.define();
27017
27018
  function initialState$4() {
27018
27019
  return {};
27019
27020
  }
@@ -27609,9 +27610,23 @@ ${c.sqlString}
27609
27610
  }
27610
27611
  });
27611
27612
  }
27613
+ var MARKDOWN_AUTORUN_USER_EVENTS = [
27614
+ "input",
27615
+ "delete",
27616
+ "undo",
27617
+ "redo"
27618
+ ];
27619
+ function shouldAutorunMarkdownUpdate({ docChanged: e, transactions: r, predicate: c = () => true, hasFocus: d = false }) {
27620
+ return !e || !c() || r.some((e2) => e2.effects.some((e3) => e3.is(formattingChangeEffect))) ? false : r.some((e2) => e2.annotation(loroSyncAnnotation) === void 0 ? MARKDOWN_AUTORUN_USER_EVENTS.some((r2) => e2.isUserEvent(r2)) || d : false);
27621
+ }
27612
27622
  function markdownAutoRunExtension({ predicate: e }) {
27613
27623
  return EditorView.updateListener.of((r) => {
27614
- r.docChanged && r.view.hasFocus && e() && (r.transactions.some((e2) => e2.effects.some((e3) => e3.is(formattingChangeEffect))) || r.view.state.facet(cellActionsState).onRun());
27624
+ shouldAutorunMarkdownUpdate({
27625
+ docChanged: r.docChanged,
27626
+ transactions: r.transactions,
27627
+ predicate: e,
27628
+ hasFocus: r.view.hasFocus
27629
+ }) && r.view.state.facet(cellActionsState).onRun();
27615
27630
  });
27616
27631
  }
27617
27632
  const pythonCompletionSource = async (e) => {
@@ -70858,7 +70873,7 @@ Image URL: ${r.imageUrl}`)), contextToXml({
70858
70873
  return Logger.warn("Failed to get version from mount config"), null;
70859
70874
  }
70860
70875
  }
70861
- const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.21.2-dev36"), showCodeInRunModeAtom = atom(true);
70876
+ const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.21.2-dev39"), showCodeInRunModeAtom = atom(true);
70862
70877
  atom(null);
70863
70878
  var import_compiler_runtime$89 = require_compiler_runtime();
70864
70879
  function useKeydownOnElement(e, r) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.21.2-dev36",
3
+ "version": "0.21.2-dev39",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,114 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ // @vitest-environment jsdom
3
+
4
+ import {
5
+ EditorState,
6
+ Transaction,
7
+ type TransactionSpec,
8
+ } from "@codemirror/state";
9
+ import { describe, expect, it } from "vitest";
10
+ import { formattingChangeEffect } from "../../format";
11
+ import { loroSyncAnnotation } from "../../rtc/loro/sync";
12
+ import { exportedForTesting } from "../extensions";
13
+
14
+ const { shouldAutorunMarkdownUpdate } = exportedForTesting;
15
+
16
+ function createTransaction(spec: TransactionSpec) {
17
+ return EditorState.create({ doc: "" }).update(spec);
18
+ }
19
+
20
+ describe("shouldAutorunMarkdownUpdate", () => {
21
+ it.each([
22
+ "input.type",
23
+ "delete.backward",
24
+ "undo",
25
+ "redo",
26
+ ])("accepts local %s transactions", (userEvent) => {
27
+ const transaction = createTransaction({
28
+ changes: { from: 0, insert: "#" },
29
+ annotations: [Transaction.userEvent.of(userEvent)],
30
+ });
31
+
32
+ expect(
33
+ shouldAutorunMarkdownUpdate({
34
+ docChanged: transaction.docChanged,
35
+ transactions: [transaction],
36
+ }),
37
+ ).toBe(true);
38
+ });
39
+
40
+ it("ignores formatting changes", () => {
41
+ const transaction = createTransaction({
42
+ changes: { from: 0, insert: "#" },
43
+ annotations: [Transaction.userEvent.of("input.type")],
44
+ effects: [formattingChangeEffect.of(true)],
45
+ });
46
+
47
+ expect(
48
+ shouldAutorunMarkdownUpdate({
49
+ docChanged: transaction.docChanged,
50
+ transactions: [transaction],
51
+ }),
52
+ ).toBe(false);
53
+ });
54
+
55
+ it("ignores RTC sync transactions", () => {
56
+ const transaction = createTransaction({
57
+ changes: { from: 0, insert: "#" },
58
+ annotations: [
59
+ Transaction.userEvent.of("input.type"),
60
+ loroSyncAnnotation.of(true),
61
+ ],
62
+ });
63
+
64
+ expect(
65
+ shouldAutorunMarkdownUpdate({
66
+ docChanged: transaction.docChanged,
67
+ transactions: [transaction],
68
+ hasFocus: true,
69
+ }),
70
+ ).toBe(false);
71
+ });
72
+
73
+ it("ignores programmatic doc changes without a user event", () => {
74
+ const transaction = createTransaction({
75
+ changes: { from: 0, insert: "#" },
76
+ });
77
+
78
+ expect(
79
+ shouldAutorunMarkdownUpdate({
80
+ docChanged: transaction.docChanged,
81
+ transactions: [transaction],
82
+ }),
83
+ ).toBe(false);
84
+ });
85
+
86
+ it("allows focused local doc changes without user event annotations", () => {
87
+ const transaction = createTransaction({
88
+ changes: { from: 0, insert: "#" },
89
+ });
90
+
91
+ expect(
92
+ shouldAutorunMarkdownUpdate({
93
+ docChanged: transaction.docChanged,
94
+ transactions: [transaction],
95
+ hasFocus: true,
96
+ }),
97
+ ).toBe(true);
98
+ });
99
+
100
+ it("honors the predicate gate", () => {
101
+ const transaction = createTransaction({
102
+ changes: { from: 0, insert: "#" },
103
+ annotations: [Transaction.userEvent.of("input.type")],
104
+ });
105
+
106
+ expect(
107
+ shouldAutorunMarkdownUpdate({
108
+ docChanged: transaction.docChanged,
109
+ transactions: [transaction],
110
+ predicate: () => false,
111
+ }),
112
+ ).toBe(false);
113
+ });
114
+ });
@@ -2,9 +2,15 @@
2
2
 
3
3
  import { closeCompletion, completionStatus } from "@codemirror/autocomplete";
4
4
  import { type Extension, Prec } from "@codemirror/state";
5
- import { EditorView, type KeyBinding, keymap } from "@codemirror/view";
5
+ import {
6
+ EditorView,
7
+ type KeyBinding,
8
+ keymap,
9
+ type ViewUpdate,
10
+ } from "@codemirror/view";
6
11
  import { createTracebackInfoAtom } from "@/core/cells/cells";
7
12
  import { type CellId, HTMLCellId, SCRATCH_CELL_ID } from "@/core/cells/ids";
13
+ import { loroSyncAnnotation } from "@/core/codemirror/rtc/loro/sync";
8
14
  import type { KeymapConfig } from "@/core/config/config-schema";
9
15
  import type { HotkeyProvider } from "@/core/hotkeys/hotkeys";
10
16
  import { duplicateWithCtrlModifier } from "@/core/hotkeys/shortcuts";
@@ -330,6 +336,53 @@ function cellCodeEditing(hotkeys: HotkeyProvider): Extension[] {
330
336
  return [onChangePlugin, formatKeymapExtension(hotkeys)];
331
337
  }
332
338
 
339
+ const MARKDOWN_AUTORUN_USER_EVENTS = ["input", "delete", "undo", "redo"];
340
+
341
+ function shouldAutorunMarkdownUpdate({
342
+ docChanged,
343
+ transactions,
344
+ predicate = () => true,
345
+ hasFocus = false,
346
+ }: Pick<ViewUpdate, "docChanged" | "transactions"> & {
347
+ predicate?: () => boolean;
348
+ hasFocus?: boolean;
349
+ }): boolean {
350
+ // If the doc didn't change, ignore.
351
+ if (!docChanged) {
352
+ return false;
353
+ }
354
+
355
+ // The caller decides when markdown autorun is allowed, e.g. not for
356
+ // f-strings where rerunning on every keystroke is usually incorrect.
357
+ if (!predicate()) {
358
+ return false;
359
+ }
360
+
361
+ // This happens on mount when we start in markdown mode.
362
+ // Ignore formatting changes so language switches don't trigger autorun.
363
+ const isFormattingChange = transactions.some((tr) =>
364
+ tr.effects.some((effect) => effect.is(formattingChangeEffect)),
365
+ );
366
+ if (isFormattingChange) {
367
+ return false;
368
+ }
369
+
370
+ return transactions.some((tr) => {
371
+ // Ignore RTC sync changes to avoid duplicate runs from remote edits.
372
+ if (tr.annotation(loroSyncAnnotation) !== undefined) {
373
+ return false;
374
+ }
375
+
376
+ // Prefer explicit local edit transactions, but keep a focused fallback for
377
+ // local rewrite paths like split-cell, which can update markdown content
378
+ // without annotating a user event.
379
+ return (
380
+ MARKDOWN_AUTORUN_USER_EVENTS.some((kind) => tr.isUserEvent(kind)) ||
381
+ hasFocus
382
+ );
383
+ });
384
+ }
385
+
333
386
  /**
334
387
  * Extension for auto-running markdown cells
335
388
  */
@@ -339,30 +392,16 @@ export function markdownAutoRunExtension({
339
392
  predicate: () => boolean;
340
393
  }): Extension {
341
394
  return EditorView.updateListener.of((update) => {
342
- // If the doc didn't change, ignore
343
- if (!update.docChanged) {
344
- return;
345
- }
346
-
347
- // If not focused, ignore
348
- // This can cause multiple runs when in RTC mode
349
- if (!update.view.hasFocus) {
350
- return;
351
- }
352
-
353
- if (!predicate()) {
395
+ if (
396
+ !shouldAutorunMarkdownUpdate({
397
+ docChanged: update.docChanged,
398
+ transactions: update.transactions,
399
+ predicate,
400
+ hasFocus: update.view.hasFocus,
401
+ })
402
+ ) {
354
403
  return;
355
404
  }
356
-
357
- // This happens on mount when we start in markdown mode
358
- const isFormattingChange = update.transactions.some((tr) =>
359
- tr.effects.some((effect) => effect.is(formattingChangeEffect)),
360
- );
361
- if (isFormattingChange) {
362
- // Ignore formatting changes
363
- return;
364
- }
365
-
366
405
  const actions = update.view.state.facet(cellActionsState);
367
406
  actions.onRun();
368
407
  });
@@ -388,3 +427,7 @@ export function cellBundle({
388
427
  ),
389
428
  ];
390
429
  }
430
+
431
+ export const exportedForTesting = {
432
+ shouldAutorunMarkdownUpdate,
433
+ };
@@ -0,0 +1,52 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { EditorState } from "@codemirror/state";
4
+ import type { EditorView } from "@codemirror/view";
5
+ import { LoroDoc, LoroText } from "loro-crdt";
6
+ import { describe, expect, it, vi } from "vitest";
7
+ import { LoroSyncPluginValue, loroSyncAnnotation } from "../sync";
8
+
9
+ describe("LoroSyncPluginValue", () => {
10
+ it("annotates the initial reconciliation dispatch as RTC sync", async () => {
11
+ const dispatch = vi.fn();
12
+ const view = {
13
+ state: EditorState.create({ doc: "local" }),
14
+ dispatch,
15
+ } as unknown as EditorView;
16
+
17
+ const doc = new LoroDoc();
18
+ const text = doc
19
+ .getMap("codes")
20
+ .getOrCreateContainer("cell-1", new LoroText());
21
+ text.insert(0, "remote");
22
+
23
+ const plugin = new LoroSyncPluginValue(
24
+ view,
25
+ doc,
26
+ ["codes", "cell-1"],
27
+ () => text,
28
+ );
29
+
30
+ await Promise.resolve();
31
+
32
+ expect(dispatch).toHaveBeenCalledTimes(1);
33
+ expect(dispatch).toHaveBeenCalledWith(
34
+ expect.objectContaining({
35
+ changes: [
36
+ {
37
+ from: 0,
38
+ to: view.state.doc.length,
39
+ insert: "remote",
40
+ },
41
+ ],
42
+ annotations: [
43
+ expect.objectContaining({
44
+ type: loroSyncAnnotation,
45
+ }),
46
+ ],
47
+ }),
48
+ );
49
+
50
+ plugin.destroy();
51
+ });
52
+ });
@@ -72,6 +72,7 @@ export class LoroSyncPluginValue implements PluginValue {
72
72
  insert: text.toString(),
73
73
  },
74
74
  ],
75
+ annotations: [loroSyncAnnotation.of(this)],
75
76
  });
76
77
  });
77
78
  }