@marimo-team/islands 0.25.3-dev206 → 0.25.3-dev208

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/{ConnectedDataExplorerComponent-DFynurqb.js → ConnectedDataExplorerComponent-ApTYh0V3.js} +4 -4
  2. package/dist/{ErrorBoundary-DZyGV5dX.js → ErrorBoundary-Dvf0WGeX.js} +2 -2
  3. package/dist/{abnfDiagram-VCTEODGH-DGkUyNdz.js → abnfDiagram-VCTEODGH-uo9xhZY0.js} +2 -2
  4. package/dist/{architectureDiagram-5GKGNRK7-Bav9J7uF.js → architectureDiagram-5GKGNRK7-Hz6eyeOD.js} +1 -1
  5. package/dist/assets/__vite-browser-external-CBRgnYY0.js +1 -0
  6. package/dist/assets/{worker-DodjWT_Z.js → worker-DiPNP__U.js} +2 -2
  7. package/dist/{chat-ui-Dorq26Pp.js → chat-ui-BTA1I5i1.js} +7 -7
  8. package/dist/{chunk-IMKFNOWR-1UCTsTqh.js → chunk-IMKFNOWR-DBM2i23k.js} +1 -1
  9. package/dist/{chunk-SHT3W25Y-Bige_rTb.js → chunk-SHT3W25Y-L8KQLLBy.js} +1 -1
  10. package/dist/{chunk-SVP7TREG-Cmr0Anfb.js → chunk-SVP7TREG-BOUJKJEP.js} +1 -1
  11. package/dist/{chunk-TICWLB2K-IWykFldb.js → chunk-TICWLB2K-DY3CzFjX.js} +1 -1
  12. package/dist/{classDiagram-ZZMXUADV-BH73ECRQ.js → classDiagram-ZZMXUADV-s9iwW-q4.js} +2 -2
  13. package/dist/{classDiagram-v2-VYDZK3BY-CCuCgq0H.js → classDiagram-v2-VYDZK3BY-2GI5CjGB.js} +2 -2
  14. package/dist/{code-visibility-CFFYYeyU.js → code-visibility-DosIqTl8.js} +4 -4
  15. package/dist/{common-B9xea2wZ.js → common-DGtdw_Pb.js} +9 -9
  16. package/dist/{cynefinDiagram-5FMLGOSQ-CWtL7MIJ.js → cynefinDiagram-5FMLGOSQ-DOxnhBK2.js} +1 -1
  17. package/dist/{dagre-GXQ25YYZ-DsVO2pVC.js → dagre-GXQ25YYZ-D1AhEfpM.js} +1 -1
  18. package/dist/{data-grid-overlay-editor-De3J2jxB.js → data-grid-overlay-editor-DcBU9xox.js} +1 -1
  19. package/dist/{diagram-S7CK7UJ4-BuAktgZv.js → diagram-S7CK7UJ4-DV_GebF_.js} +1 -1
  20. package/dist/{diagram-UQ7AKVKN-DlkAXHu5.js → diagram-UQ7AKVKN-6dNzGFfD.js} +1 -1
  21. package/dist/{diagram-VX7I27RA-B9U9H0E8.js → diagram-VX7I27RA-DUh9jsfi.js} +1 -1
  22. package/dist/{diagram-Z3DM3KII-BqmfxzUL.js → diagram-Z3DM3KII-Cug-qcQ8.js} +1 -1
  23. package/dist/{dropdown-menu-BFQYzWmC.js → dropdown-menu-dlEm41Bt.js} +1 -1
  24. package/dist/{ebnfDiagram-PWID7BFC-CH5t8ATi.js → ebnfDiagram-PWID7BFC-d5LKx7Vb.js} +2 -2
  25. package/dist/{erDiagram-RLTQ6QDP-DdIo2BIb.js → erDiagram-RLTQ6QDP-BwvlkcID.js} +1 -1
  26. package/dist/{error-entries-B0bcEwXm.js → error-entries-CVUHA5r_.js} +3875 -3884
  27. package/dist/{feedback-button-DXpmx6ff.js → feedback-button-4aNGVv5T.js} +5 -5
  28. package/dist/{flowDiagram-HODETNUW-BmYk_-Na.js → flowDiagram-HODETNUW-CJcSoFer.js} +2 -2
  29. package/dist/{formats-DyYnckF5.js → formats-Ry7c-aIV.js} +1 -1
  30. package/dist/{glide-data-editor-BevQ8Zdz.js → glide-data-editor-CwG-6cpk.js} +5 -5
  31. package/dist/{globals-Beuc24As.js → globals-CE02RdV9.js} +2 -2
  32. package/dist/{html-to-image-BOd7ryZJ.js → html-to-image--E3uFbJ9.js} +5 -5
  33. package/dist/{infoDiagram-27XIBGKW-DvYh-Sbk.js → infoDiagram-27XIBGKW-Cezgppw3.js} +1 -1
  34. package/dist/{ishikawaDiagram-5VMMS53U-BdHnYq8s.js → ishikawaDiagram-5VMMS53U-BahZQwJz.js} +1 -1
  35. package/dist/{kanban-definition-UXKFOSKX-Bf8CT_tU.js → kanban-definition-UXKFOSKX-Cvfn_Obm.js} +1 -1
  36. package/dist/main.js +22 -22
  37. package/dist/{mermaid-DBpOnw8c.js → mermaid-BrdwbArF.js} +34 -34
  38. package/dist/{mindmap-definition-YA3MSWOX-DkYG8ltR.js → mindmap-definition-YA3MSWOX-uB4B2d-J.js} +1 -1
  39. package/dist/{number-overlay-editor-DX_x9WUo.js → number-overlay-editor-CDwQMUeU.js} +1 -1
  40. package/dist/{pegDiagram-XKGWAZYB-DM7BQ0ip.js → pegDiagram-XKGWAZYB-CYaIwAmH.js} +2 -2
  41. package/dist/{pieDiagram-E7YTZNPT-CPFGg4YS.js → pieDiagram-E7YTZNPT-BJ1oUxVv.js} +1 -1
  42. package/dist/{process-output-8BbxxGrz.js → process-output-SjJxB1wh.js} +2 -2
  43. package/dist/{railroadDiagram-O6MQD6OU-4rUJlIpQ.js → railroadDiagram-O6MQD6OU-BqKSaBux.js} +2 -2
  44. package/dist/{requirementDiagram-BXWQKSXE-F6ffUUQC.js → requirementDiagram-BXWQKSXE-DwFhgDhH.js} +1 -1
  45. package/dist/{reveal-component-BMw2TipA.js → reveal-component-ntNLwRBO.js} +6 -6
  46. package/dist/{sequenceDiagram-WJ2MYXX4-tNU95HVS.js → sequenceDiagram-WJ2MYXX4-L7ENjfKF.js} +1 -1
  47. package/dist/{spec-Alkla7dB.js → spec-BcJ9ncAY.js} +1 -1
  48. package/dist/{stateDiagram-D77RDMKH-BA8Modqv.js → stateDiagram-D77RDMKH-CVf88u-p.js} +2 -2
  49. package/dist/{stateDiagram-v2-MP3YSRHH-CFmickht.js → stateDiagram-v2-MP3YSRHH-DvpkQ1V6.js} +2 -2
  50. package/dist/style.css +1 -1
  51. package/dist/{swimlanes-42K2YHIH-DBiYZZZc.js → swimlanes-42K2YHIH-DIuFHbVE.js} +1 -1
  52. package/dist/{swimlanesDiagram-VR7AAH4N-CsgR851k.js → swimlanesDiagram-VR7AAH4N-CAqHAziW.js} +2 -2
  53. package/dist/{timeline-definition-24CTP7MA-Dfj0CMXY.js → timeline-definition-24CTP7MA-xO4EQqBm.js} +1 -1
  54. package/dist/{toDate-B0-Y0SEW.js → toDate-BZZ64K_m.js} +1 -5
  55. package/dist/{useAsyncData-Phxf6GCd.js → useAsyncData-Dqw22wZQ.js} +1 -1
  56. package/dist/{useDeepCompareMemoize-BXv7DUcU.js → useDeepCompareMemoize-D3gozcjJ.js} +1 -1
  57. package/dist/{useLifecycle-DDDsO4O2.js → useLifecycle-CaZi4wTw.js} +1 -1
  58. package/dist/{useTheme-DbBkeP6Z.js → useTheme-TzPgWXwB.js} +1 -4
  59. package/dist/{vega-component-C_dhEynE.js → vega-component-Blvspl7v.js} +5 -5
  60. package/dist/{vennDiagram-4TSXK5OY-fJ83Z7FE.js → vennDiagram-4TSXK5OY-DzCi-lRh.js} +1 -1
  61. package/dist/{wardleyDiagram-VM6X3IG4-DbO9cD15.js → wardleyDiagram-VM6X3IG4-CW48xa0n.js} +1 -1
  62. package/dist/{xychartDiagram-S5SC5T6Z-DmsdQJG7.js → xychartDiagram-S5SC5T6Z-UPxGGI2B.js} +1 -1
  63. package/package.json +2 -6
  64. package/src/components/app-config/user-config-form.tsx +0 -56
  65. package/src/components/editor/cell/code/cell-editor.tsx +11 -100
  66. package/src/components/editor/chrome/wrapper/footer.tsx +0 -2
  67. package/src/core/cells/__tests__/document.test.ts +0 -18
  68. package/src/core/cells/document.ts +6 -9
  69. package/src/core/codemirror/cells/__tests__/extensions.test.ts +1 -46
  70. package/src/core/codemirror/cells/extensions.ts +1 -19
  71. package/src/core/config/config-schema.ts +0 -1
  72. package/src/core/config/feature-flag.tsx +0 -2
  73. package/src/core/runtime/__tests__/runtime.test.ts +0 -16
  74. package/src/core/runtime/runtime.ts +0 -8
  75. package/src/css/app/codemirror.css +1 -1
  76. package/dist/assets/__vite-browser-external-BCJsx8QS.js +0 -1
  77. package/src/components/editor/cell/code/__tests__/rtc-editor-config.test.ts +0 -50
  78. package/src/components/editor/chrome/wrapper/footer-items/rtc-status.tsx +0 -65
  79. package/src/core/codemirror/rtc/extension.ts +0 -464
  80. package/src/core/codemirror/rtc/loro/__tests__/sync.test.ts +0 -52
  81. package/src/core/codemirror/rtc/loro/awareness.ts +0 -546
  82. package/src/core/codemirror/rtc/loro/colors.ts +0 -38
  83. package/src/core/codemirror/rtc/loro/sync.ts +0 -169
  84. package/src/core/rtc/state.ts +0 -34
@@ -204,7 +204,6 @@ export const UserConfigSchema = z
204
204
  experimental: z
205
205
  .looseObject({
206
206
  markdown: z.boolean().optional(),
207
- rtc: z.boolean().optional(),
208
207
  // Add new experimental features here
209
208
  })
210
209
  // Pass through so that we don't remove any extra keys that the user has added.
@@ -8,7 +8,6 @@ import { getResolvedMarimoConfig } from "./config";
8
8
  export interface ExperimentalFeatures {
9
9
  markdown: boolean; // Used in playground (community cloud)
10
10
  wasm_layouts: boolean; // Used in playground (community cloud)
11
- rtc_v2: boolean;
12
11
  cache_panel: boolean;
13
12
  external_agents: boolean;
14
13
  debugger: boolean; // Live frame-watching debugger (gutter breakpoints + pdb)
@@ -19,7 +18,6 @@ export interface ExperimentalFeatures {
19
18
  const defaultValues: ExperimentalFeatures = {
20
19
  markdown: true,
21
20
  wasm_layouts: false,
22
- rtc_v2: false,
23
21
  cache_panel: import.meta.env.DEV,
24
22
  external_agents: import.meta.env.DEV,
25
23
  debugger: false,
@@ -143,13 +143,9 @@ describe("RuntimeManager", () => {
143
143
  );
144
144
 
145
145
  const wsUrl = runtime.getWsURL("s_123" as SessionId);
146
- const wsSyncUrl = runtime.getWsSyncURL("s_123" as SessionId);
147
146
  const terminalUrl = runtime.getTerminalWsURL();
148
147
 
149
148
  expect(wsUrl.searchParams.get("access_token")).toBe("my-secret-token");
150
- expect(wsSyncUrl.searchParams.get("access_token")).toBe(
151
- "my-secret-token",
152
- );
153
149
  expect(terminalUrl.searchParams.get("access_token")).toBe(
154
150
  "my-secret-token",
155
151
  );
@@ -274,18 +270,6 @@ describe("RuntimeManager", () => {
274
270
  });
275
271
  });
276
272
 
277
- describe("getWsSyncURL", () => {
278
- it("should return WebSocket Sync URL", () => {
279
- const runtime = new RuntimeManager(mockConfig);
280
- const sessionId = "1234" as SessionId;
281
- const url = runtime.getWsSyncURL(sessionId);
282
-
283
- expect(url.protocol).toBe("wss:");
284
- expect(url.pathname).toBe("/ws_sync");
285
- expect(url.searchParams.get("session_id")).toBe(sessionId);
286
- });
287
- });
288
-
289
273
  describe("getTerminalWsURL", () => {
290
274
  it("should include dimensions in the terminal handshake", () => {
291
275
  const runtime = new RuntimeManager(mockConfig);
@@ -184,14 +184,6 @@ export class RuntimeManager {
184
184
  return url;
185
185
  }
186
186
 
187
- /**
188
- * The WebSocket Sync URL of the runtime, for real-time updates.
189
- */
190
- public getWsSyncURL(sessionId: SessionId): URL {
191
- const url = this.getSessionURL("/ws_sync", sessionId);
192
- return asWsUrl(this.appendCrossOriginAuth(url).toString());
193
- }
194
-
195
187
  /**
196
188
  * The WebSocket URL of the terminal.
197
189
  */
@@ -34,7 +34,7 @@
34
34
  }
35
35
 
36
36
  .marimo-cell .cm-scroller {
37
- /* this needs to be visible to show RTC cursors and inline AI edit buttons */
37
+ /* this needs to be visible to show inline AI edit buttons */
38
38
  overflow: visible;
39
39
  }
40
40
 
@@ -1 +0,0 @@
1
- import{t as e}from"./worker-DodjWT_Z.js";var t=e(((e,t)=>{t.exports={}}));export default t();
@@ -1,50 +0,0 @@
1
- /* Copyright 2026 Marimo. All rights reserved. */
2
-
3
- import { EditorState } from "@codemirror/state";
4
- import { describe, expect, it } from "vitest";
5
- import { composeRtcEditorConfig } from "../cell-editor";
6
-
7
- describe("composeRtcEditorConfig", () => {
8
- it("does not accumulate RTC extensions across reconfigurations", () => {
9
- const baseExtension = EditorState.readOnly.of(false);
10
- const baseExtensions = [baseExtension];
11
- const firstRtc = {
12
- code: "first RTC code",
13
- extension: EditorState.tabSize.of(2),
14
- };
15
- const secondRtc = {
16
- code: "second RTC code",
17
- extension: EditorState.tabSize.of(4),
18
- };
19
-
20
- const firstConfig = composeRtcEditorConfig(
21
- "local code",
22
- baseExtensions,
23
- firstRtc,
24
- );
25
- const secondConfig = composeRtcEditorConfig(
26
- "local code",
27
- baseExtensions,
28
- secondRtc,
29
- );
30
-
31
- expect(baseExtensions).toEqual([baseExtension]);
32
- expect(firstConfig).toEqual({
33
- code: firstRtc.code,
34
- extensions: [baseExtension, firstRtc.extension],
35
- });
36
- expect(secondConfig).toEqual({
37
- code: secondRtc.code,
38
- extensions: [baseExtension, secondRtc.extension],
39
- });
40
- });
41
-
42
- it("reuses the base configuration when RTC is disabled", () => {
43
- const extensions = [EditorState.readOnly.of(false)];
44
-
45
- const config = composeRtcEditorConfig("local code", extensions, undefined);
46
-
47
- expect(config).toEqual({ code: "local code", extensions });
48
- expect(config.extensions).toBe(extensions);
49
- });
50
- });
@@ -1,65 +0,0 @@
1
- /* Copyright 2026 Marimo. All rights reserved. */
2
-
3
- import { useAtom, useAtomValue } from "jotai";
4
- import { UsersIcon } from "lucide-react";
5
- import type React from "react";
6
- import { useState } from "react";
7
- import { Input } from "@/components/ui/input";
8
- import {
9
- Popover,
10
- PopoverContent,
11
- PopoverTrigger,
12
- } from "@/components/ui/popover";
13
- import { connectedDocAtom } from "@/core/codemirror/rtc/extension";
14
- import { isRtcEnabled, usernameAtom } from "@/core/rtc/state";
15
- import { FooterItem } from "../footer-item";
16
-
17
- export const RTCStatus: React.FC = () => {
18
- const connectedDoc = useAtomValue(connectedDocAtom);
19
- const [username, setUsername] = useAtom(usernameAtom);
20
- const [open, setOpen] = useState(false);
21
-
22
- if (!isRtcEnabled()) {
23
- return null;
24
- }
25
-
26
- if (connectedDoc === "disabled") {
27
- return null;
28
- }
29
-
30
- const tooltip = connectedDoc
31
- ? "Real-time collaboration active"
32
- : "Connecting to real-time collaboration";
33
-
34
- return (
35
- <Popover open={open} onOpenChange={setOpen}>
36
- <PopoverTrigger asChild={true}>
37
- <FooterItem
38
- tooltip={tooltip}
39
- selected={false}
40
- data-testid="footer-rtc-status"
41
- >
42
- <UsersIcon className="w-4 h-4" />
43
- </FooterItem>
44
- </PopoverTrigger>
45
- <PopoverContent className="w-80">
46
- <div className="space-y-4">
47
- <div className="space-y-2">
48
- <h4 className="font-medium leading-none">Username</h4>
49
- <p className="text-sm text-muted-foreground">
50
- Set your username for real-time collaboration
51
- </p>
52
- </div>
53
- <Input
54
- value={username}
55
- autoCapitalize="off"
56
- autoComplete="off"
57
- autoCorrect="off"
58
- onChange={(e) => setUsername(e.target.value)}
59
- placeholder="Enter your username"
60
- />
61
- </div>
62
- </PopoverContent>
63
- </Popover>
64
- );
65
- };
@@ -1,464 +0,0 @@
1
- /* Copyright 2026 Marimo. All rights reserved. */
2
-
3
- import type { Extension } from "@codemirror/state";
4
- import { EditorView, ViewPlugin } from "@codemirror/view";
5
- import { dequal as isEqual } from "dequal";
6
- import { atom } from "jotai";
7
- import { Awareness, LoroDoc, LoroMap, LoroText } from "loro-crdt";
8
- import ReconnectingWebSocket from "partysocket/ws";
9
- import type { CellId } from "@/core/cells/ids";
10
- import { getSessionId } from "@/core/kernel/session";
11
- import { getInitialAppMode } from "@/core/mode";
12
- import { waitForConnectionOpen } from "@/core/network/connection";
13
- import { isRtcEnabled, usernameAtom } from "@/core/rtc/state";
14
- import { getRuntimeManager } from "@/core/runtime/config";
15
- import { store } from "@/core/state/jotai";
16
- import { isWasm } from "@/core/wasm/utils";
17
- import { invariant } from "@/utils/invariant";
18
- import { Logger } from "@/utils/Logger";
19
- import { once } from "@/utils/once";
20
- import {
21
- getInitialLanguageAdapter,
22
- languageAdapterState,
23
- setLanguageAdapter,
24
- switchLanguage,
25
- } from "../language/extension";
26
- import {
27
- languageMetadataField,
28
- setLanguageMetadata,
29
- updateLanguageMetadata,
30
- } from "../language/metadata";
31
- import type { LanguageAdapterType } from "../language/types";
32
- import type { AwarenessState, ScopeId, Uid, UserState } from "./loro/awareness";
33
- import {
34
- AwarenessPlugin,
35
- createCursorLayer,
36
- createSelectionLayer,
37
- loroCursorTheme,
38
- RemoteAwarenessPlugin,
39
- remoteAwarenessStateField,
40
- } from "./loro/awareness";
41
- import { getColor } from "./loro/colors";
42
- import { loroSyncAnnotation, loroSyncPlugin } from "./loro/sync";
43
-
44
- const logger = Logger.get("rtc");
45
- const awarenessLogger = logger.get("awareness").disabled();
46
-
47
- const AWARENESS_PREFIX = "awareness:";
48
-
49
- // Utility functions for message handling
50
- function prefixMessage(
51
- token: string,
52
- message: Uint8Array,
53
- ): Uint8Array<ArrayBuffer> {
54
- const tokenBytes = new TextEncoder().encode(token);
55
- return new Uint8Array([...tokenBytes, ...message]);
56
- }
57
-
58
- function hasPrefix(data: Uint8Array, prefix: string): boolean {
59
- const decoder = new TextDecoder();
60
- const dataPrefix = decoder.decode(data.slice(0, prefix.length));
61
- return dataPrefix === prefix;
62
- }
63
-
64
- function removePrefix(data: Uint8Array, prefix: string): Uint8Array {
65
- return data.slice(prefix.length);
66
- }
67
-
68
- export const connectedDocAtom = atom<LoroDoc | "disabled" | undefined>(
69
- undefined,
70
- );
71
-
72
- // Create a Loro document
73
- const doc = new LoroDoc();
74
- const awareness = new Awareness<AwarenessState>(doc.peerIdStr);
75
- // const undoManager = new UndoManager(doc, {});
76
-
77
- // Create a websocket connection to the server
78
- const getWs = once(() => {
79
- logger.debug("creating websocket");
80
-
81
- const runtimeManager = getRuntimeManager();
82
- const url = runtimeManager.getWsSyncURL(getSessionId()).toString();
83
-
84
- // Create the websocket, but don't connect it yet
85
- const ws = new ReconnectingWebSocket(url, undefined, {
86
- // We don't want Infinity retries
87
- maxRetries: 10,
88
- debug: false,
89
- startClosed: true,
90
- connectionTimeout: 10_000,
91
- });
92
-
93
- // Start the connection
94
- Promise.resolve().then(async () => {
95
- // First wait the main /ws connection to be open
96
- // This is to ensure the LoroDoc is created on the server
97
- await waitForConnectionOpen();
98
-
99
- // Don't open the websocket if not in edit mode
100
- if (getInitialAppMode() !== "edit") {
101
- return;
102
- }
103
-
104
- // Now open the websocket
105
- ws.reconnect();
106
- });
107
-
108
- // Receive updates from the server
109
- ws.addEventListener("message", async (event) => {
110
- const blob: Blob = event.data;
111
- const bytes = await blob.arrayBuffer();
112
- const data = new Uint8Array(bytes);
113
-
114
- // Handle awareness update
115
- if (hasPrefix(data, AWARENESS_PREFIX)) {
116
- const awarenessData = removePrefix(data, AWARENESS_PREFIX);
117
- awareness.apply(awarenessData);
118
- awarenessLogger.debug("applied awareness update");
119
- return;
120
- }
121
-
122
- // Handle doc update
123
- doc.import(data);
124
- logger.debug("imported doc change. new doc:", doc.toJSON());
125
-
126
- // Set the active doc only once the first message is received
127
- // This is to ensure the LoroDoc is created on the server, and not the client.
128
- if (store.get(connectedDocAtom) !== doc) {
129
- store.set(connectedDocAtom, doc);
130
- }
131
- });
132
-
133
- // Handle open event
134
- ws.addEventListener("open", () => {
135
- logger.debug("websocket open");
136
- });
137
-
138
- // Handle close event
139
- ws.addEventListener("close", (e) => {
140
- logger.warn("websocket close", e);
141
-
142
- // Remove the active doc
143
- if (store.get(connectedDocAtom) === doc) {
144
- store.set(connectedDocAtom, undefined);
145
- }
146
- });
147
-
148
- return ws;
149
- });
150
-
151
- // Kickoff the WS connection for edit mode
152
- if (isRtcEnabled()) {
153
- getWs();
154
- }
155
-
156
- // Send local updates to the server
157
- doc.subscribeLocalUpdates((update) => {
158
- logger.debug("local update, sending to server");
159
- const ws = getWs();
160
- ws.send(new Uint8Array(update));
161
- });
162
-
163
- // Handle awareness changes
164
- awareness.addListener((updates, origin) => {
165
- const changes = [...updates.added, ...updates.removed, ...updates.updated];
166
- if (origin === "local") {
167
- awarenessLogger.debug("awareness changes", changes);
168
- const ws = getWs();
169
- ws.send(prefixMessage(AWARENESS_PREFIX, awareness.encode(changes)));
170
- }
171
- });
172
-
173
- export function realTimeCollaboration(
174
- cellId: CellId,
175
- initialCode = "",
176
- ): { extension: Extension; code: string } {
177
- if (isWasm()) {
178
- return {
179
- extension: [],
180
- code: initialCode,
181
- };
182
- }
183
-
184
- // Get or create the code container
185
- const hasPath = doc.getByPath(`codes/${cellId}`) !== undefined;
186
- const loroText = doc
187
- .getMap("codes")
188
- .getOrCreateContainer(cellId, new LoroText());
189
- if (!hasPath) {
190
- logger.log("initializing code for new cell", initialCode);
191
- loroText.insert(0, initialCode);
192
- }
193
-
194
- const userState: UserState = {
195
- // We use getters to ensure the user name is up-to-date
196
- get name() {
197
- return store.get(usernameAtom) || "Anonymous";
198
- },
199
- get colorClassName() {
200
- return getColor(this.name);
201
- },
202
- };
203
-
204
- return {
205
- code: initialCode.toString(),
206
- extension: [
207
- languageObserverExtension(cellId),
208
- languageListenerExtension(cellId),
209
- loroAwarenessPlugin({
210
- doc,
211
- awareness,
212
- user: userState,
213
- getTextFromDoc: () => loroText,
214
- cellId,
215
- }),
216
- loroSyncPlugin(doc, ["codes", cellId], () => {
217
- return loroText;
218
- }),
219
- ],
220
- };
221
- }
222
-
223
- /**
224
- * Create a view plugin to observe language changes
225
- * When the language changes, we need to update our local language
226
- * Server -> Local
227
- * @param cellId - The cell id
228
- * @returns Extension
229
- */
230
- function languageObserverExtension(cellId: CellId) {
231
- const syncLanguage = (view: EditorView, language: LoroText) => {
232
- const currentLang = view.state.field(languageAdapterState).type;
233
- const newLang = language.toString() as LanguageAdapterType;
234
- if (!newLang) {
235
- return;
236
- }
237
-
238
- if (newLang !== currentLang) {
239
- logger.debug(
240
- `[incoming] setting language type: ${currentLang} -> ${newLang}`,
241
- );
242
- switchLanguage(view, { language: newLang, keepCodeAsIs: true });
243
- }
244
- };
245
-
246
- const syncLanguageMetadata = (
247
- view: EditorView,
248
- languageMetadata: LoroMap,
249
- ) => {
250
- const previousLanguageMetadata = view.state.field(languageMetadataField);
251
- const newLanguageMetadata = languageMetadata.toJSON();
252
- if (isEqual(previousLanguageMetadata, newLanguageMetadata)) {
253
- return;
254
- }
255
-
256
- view.dispatch({
257
- effects: [updateLanguageMetadata.of(newLanguageMetadata)],
258
- annotations: [loroSyncAnnotation.of(true)],
259
- });
260
- };
261
-
262
- return ViewPlugin.define((view) => {
263
- let unsubscribeDoc: () => void;
264
-
265
- // Initialize after a single tick
266
- Promise.resolve().then(() => {
267
- // Language type
268
- const langType = doc.getByPath(`languages/${cellId}`);
269
- if (!langType) {
270
- logger.error("no language container found for cell", cellId);
271
- return;
272
- }
273
- invariant(
274
- langType instanceof LoroText,
275
- "language type is not a LoroText",
276
- );
277
-
278
- // Language metadata
279
- const langMeta = doc.getByPath(`language_metadata/${cellId}`);
280
- if (!langMeta) {
281
- logger.error("no language metadata container found for cell", cellId);
282
- return;
283
- }
284
- invariant(
285
- langMeta instanceof LoroMap,
286
- "language metadata is not a LoroMap",
287
- );
288
-
289
- // Run once
290
- syncLanguage(view, langType);
291
- syncLanguageMetadata(view, langMeta);
292
-
293
- // Subscribe to language and metadata changes
294
- unsubscribeDoc = doc.subscribe((event) => {
295
- if (event.origin === "local") {
296
- // Skip if the change is local
297
- return;
298
- }
299
-
300
- const hasAnyLanguageTypeChanges = event.events.some(
301
- (e) => e.target === langType.id,
302
- );
303
- const hasAnyLanguageMetadataChanges = event.events.some(
304
- (e) => e.target === langMeta.id,
305
- );
306
-
307
- if (hasAnyLanguageTypeChanges) {
308
- syncLanguage(view, langType);
309
- }
310
- if (hasAnyLanguageMetadataChanges) {
311
- syncLanguageMetadata(view, langMeta);
312
- }
313
- });
314
- });
315
-
316
- return {
317
- destroy() {
318
- unsubscribeDoc();
319
- },
320
- };
321
- });
322
- }
323
-
324
- /**
325
- * Listen for local language changes
326
- * This is used to sync the language with the server
327
- * Local -> Server
328
- * @returns Extension
329
- */
330
- function languageListenerExtension(cellId: CellId) {
331
- // Get or create the language type container
332
- const currentLang = doc
333
- .getMap("languages")
334
- .getOrCreateContainer(cellId, new LoroText());
335
-
336
- // Get or create the language metadata container
337
- const currentLanguageMetadata = doc
338
- .getMap("language_metadata")
339
- .getOrCreateContainer(cellId, new LoroMap());
340
-
341
- return EditorView.updateListener.of((update) => {
342
- const isInitialized = currentLang.toString() !== "";
343
-
344
- // Skip if the doc hasn't changed and the language is already set
345
- if (!update.docChanged && isInitialized) {
346
- return;
347
- }
348
-
349
- // If the language is not set, set it to the current language
350
- // and update the LoroDoc
351
- if (!isInitialized) {
352
- const language = getInitialLanguageAdapter(update.state).type;
353
- switchLanguage(update.view, { language });
354
- logger.debug("no initial language, setting default to", language);
355
-
356
- // Sync the language to the LoroDoc
357
- currentLang.delete(0, currentLang.length);
358
- currentLang.insert(0, language);
359
-
360
- // Sync the language metadata to the LoroDoc
361
- logger.debug(
362
- "no initial language metadata, setting default to",
363
- update.state.field(languageMetadataField),
364
- );
365
- const metadata = update.state.field(languageMetadataField);
366
- for (const key of Object.keys(metadata)) {
367
- currentLanguageMetadata.set(key, metadata[key]);
368
- }
369
-
370
- // Commit the changes
371
- doc.commit();
372
-
373
- return;
374
- }
375
-
376
- let hasChanges = false;
377
-
378
- for (const tr of update.transactions) {
379
- const isSyncOperation = tr.annotation(loroSyncAnnotation);
380
- if (isSyncOperation) {
381
- continue;
382
- }
383
-
384
- for (const e of tr.effects) {
385
- // Language type
386
- if (
387
- e.is(setLanguageAdapter) &&
388
- currentLang.toString() !== e.value.type
389
- ) {
390
- logger.debug(
391
- `[outgoing] language change: ${currentLang} -> ${e.value.type}`,
392
- );
393
- currentLang.delete(0, currentLang.length);
394
- currentLang.insert(0, e.value.type);
395
- hasChanges = true;
396
- }
397
-
398
- // Language metadata
399
- if (e.is(updateLanguageMetadata) || e.is(setLanguageMetadata)) {
400
- const metadata = e.value;
401
-
402
- // If it is set, we should clear the metadata first
403
- if (e.is(setLanguageMetadata)) {
404
- logger.debug("[outgoing] setting language metadata: ", metadata);
405
- currentLanguageMetadata.clear();
406
- } else {
407
- logger.debug("[outgoing] updating language metadata: ", metadata);
408
- }
409
-
410
- for (const key of Object.keys(metadata)) {
411
- currentLanguageMetadata.set(key, metadata[key]);
412
- }
413
- hasChanges = true;
414
- }
415
- }
416
- }
417
-
418
- if (hasChanges) {
419
- doc.commit();
420
- }
421
- });
422
- }
423
-
424
- /**
425
- * Create a plugin to observe awareness changes
426
- */
427
- function loroAwarenessPlugin({
428
- doc,
429
- awareness,
430
- user,
431
- getTextFromDoc,
432
- cellId,
433
- getUserId,
434
- }: {
435
- doc: LoroDoc;
436
- awareness: Awareness<AwarenessState>;
437
- user: UserState;
438
- getTextFromDoc: (doc: LoroDoc) => LoroText;
439
- cellId: CellId;
440
- getUserId?: () => Uid;
441
- }): Extension[] {
442
- const scopeId = `loro:cell:${cellId}` as ScopeId;
443
-
444
- return [
445
- remoteAwarenessStateField,
446
- createCursorLayer(),
447
- createSelectionLayer(),
448
- ViewPlugin.define(
449
- (view) =>
450
- new AwarenessPlugin(view, {
451
- doc,
452
- user,
453
- awareness,
454
- getTextFromDoc,
455
- scopeId,
456
- getUserId,
457
- }),
458
- ),
459
- ViewPlugin.define(
460
- (view) => new RemoteAwarenessPlugin(view, doc, awareness, scopeId),
461
- ),
462
- loroCursorTheme,
463
- ];
464
- }