@datalayer/lexical-loro 1.0.3 → 1.0.5

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 (113) hide show
  1. package/README.md +2 -2
  2. package/lib/Editor.js +3 -2
  3. package/lib/appSettings.js +1 -1
  4. package/lib/collab/loro/Bindings.js +7 -7
  5. package/lib/collab/loro/Debug.js +35 -35
  6. package/lib/collab/loro/LexicalCollaborationContext.js +1 -1
  7. package/lib/collab/loro/LexicalCollaborationPlugin.d.ts +6 -1
  8. package/lib/collab/loro/LexicalCollaborationPlugin.js +41 -13
  9. package/lib/collab/loro/State.js +1 -1
  10. package/lib/collab/loro/components/LoroCollaborators.js +7 -19
  11. package/lib/collab/loro/components/index.js +1 -1
  12. package/lib/collab/loro/index.js +1 -1
  13. package/lib/collab/loro/integrators/BaseIntegrator.js +1 -1
  14. package/lib/collab/loro/integrators/CounterIntegrator.js +1 -1
  15. package/lib/collab/loro/integrators/ListIntegrator.js +4 -4
  16. package/lib/collab/loro/integrators/MapIntegrator.js +4 -4
  17. package/lib/collab/loro/integrators/TextIntegrator.js +2 -2
  18. package/lib/collab/loro/integrators/TreeIntegrator.d.ts +5 -0
  19. package/lib/collab/loro/integrators/TreeIntegrator.js +184 -142
  20. package/lib/collab/loro/nodes/NodeFactory.js +26 -47
  21. package/lib/collab/loro/nodes/NodesMapper.js +19 -7
  22. package/lib/collab/loro/propagators/DecoratorNodePropagator.js +1 -1
  23. package/lib/collab/loro/propagators/ElementNodePropagator.d.ts +1 -1
  24. package/lib/collab/loro/propagators/ElementNodePropagator.js +102 -35
  25. package/lib/collab/loro/propagators/LineBreakNodePropagator.js +1 -1
  26. package/lib/collab/loro/propagators/RootNodePropagator.js +1 -1
  27. package/lib/collab/loro/propagators/TextNodePropagator.js +55 -45
  28. package/lib/collab/loro/propagators/index.d.ts +1 -1
  29. package/lib/collab/loro/propagators/index.js +1 -1
  30. package/lib/collab/loro/provider/websocket.d.ts +1 -0
  31. package/lib/collab/loro/provider/websocket.js +47 -12
  32. package/lib/collab/loro/servers/index.js +1 -1
  33. package/lib/collab/loro/servers/ws/callback.js +1 -1
  34. package/lib/collab/loro/servers/ws/server.js +1 -1
  35. package/lib/collab/loro/servers/ws/utils.js +35 -35
  36. package/lib/collab/loro/sync/SyncCursors.js +42 -16
  37. package/lib/collab/loro/sync/SyncLexicalToLoro.js +76 -37
  38. package/lib/collab/loro/sync/SyncLoroToLexical.js +32 -26
  39. package/lib/collab/loro/types/LexicalNodeData.js +1 -1
  40. package/lib/collab/loro/useCollaboration.js +173 -28
  41. package/lib/collab/loro/utils/InitialContent.js +1 -1
  42. package/lib/collab/loro/utils/Invariant.d.ts +15 -0
  43. package/lib/collab/loro/utils/Invariant.js +24 -0
  44. package/lib/collab/loro/utils/LexicalToLoro.js +1 -1
  45. package/lib/collab/loro/utils/Utils.js +1 -1
  46. package/lib/collab/loro/wsProvider.js +21 -4
  47. package/lib/collab/utils/invariant.js +1 -1
  48. package/lib/collab/utils/simpleDiffWithCursor.js +1 -1
  49. package/lib/commenting/index.js +1 -1
  50. package/lib/demo.js +1 -1
  51. package/lib/hooks/useFlashMessage.js +1 -1
  52. package/lib/hooks/useReport.js +1 -1
  53. package/lib/index.js +1 -1
  54. package/lib/nodes/AutocompleteNode.js +1 -1
  55. package/lib/nodes/EmojiNode.js +1 -1
  56. package/lib/nodes/KeywordNode.js +1 -1
  57. package/lib/nodes/LayoutContainerNode.js +1 -1
  58. package/lib/nodes/LayoutItemNode.js +1 -1
  59. package/lib/nodes/MentionNode.js +1 -1
  60. package/lib/nodes/PlaygroundNodes.js +1 -1
  61. package/lib/nodes/SpecialTextNode.js +1 -1
  62. package/lib/plugins/AutocompletePlugin/index.js +1 -1
  63. package/lib/plugins/CodeActionMenuPlugin/components/CopyButton/index.js +1 -1
  64. package/lib/plugins/CodeActionMenuPlugin/components/PrettierButton/index.js +1 -1
  65. package/lib/plugins/CodeActionMenuPlugin/utils.js +1 -1
  66. package/lib/plugins/CodeHighlightPrismPlugin/index.js +1 -1
  67. package/lib/plugins/CodeHighlightShikiPlugin/index.js +1 -1
  68. package/lib/plugins/CollapsiblePlugin/CollapsibleContainerNode.js +1 -1
  69. package/lib/plugins/CollapsiblePlugin/CollapsibleContentNode.js +1 -1
  70. package/lib/plugins/CollapsiblePlugin/CollapsibleTitleNode.js +1 -1
  71. package/lib/plugins/CollapsiblePlugin/CollapsibleUtils.js +1 -1
  72. package/lib/plugins/CollapsiblePlugin/index.js +1 -1
  73. package/lib/plugins/CounterPlugin/index.js +1 -1
  74. package/lib/plugins/DateTimePlugin/index.js +1 -1
  75. package/lib/plugins/DebugPlugin/index.js +59 -3
  76. package/lib/plugins/DragDropPastePlugin/index.js +1 -1
  77. package/lib/plugins/EmojiPickerPlugin/index.js +1 -1
  78. package/lib/plugins/EmojisPlugin/index.js +1 -1
  79. package/lib/plugins/FigmaPlugin/index.js +1 -1
  80. package/lib/plugins/KeywordsPlugin/index.js +1 -1
  81. package/lib/plugins/LayoutPlugin/LayoutPlugin.js +1 -1
  82. package/lib/plugins/MarkdownTransformers/index.js +1 -1
  83. package/lib/plugins/MaxLengthPlugin/index.js +1 -1
  84. package/lib/plugins/PageBreakPlugin/index.js +1 -1
  85. package/lib/plugins/ShortcutsPlugin/index.js +1 -1
  86. package/lib/plugins/ShortcutsPlugin/shortcuts.js +1 -1
  87. package/lib/plugins/SpecialTextPlugin/index.js +1 -1
  88. package/lib/plugins/SpeechToTextPlugin/index.js +1 -1
  89. package/lib/plugins/StickyPlugin/index.js +1 -1
  90. package/lib/plugins/TabFocusPlugin/index.js +1 -1
  91. package/lib/plugins/ToolbarPlugin/fontSize.js +1 -1
  92. package/lib/plugins/ToolbarPlugin/utils.js +1 -1
  93. package/lib/plugins/TwitterPlugin/index.js +1 -1
  94. package/lib/plugins/TypingPerfPlugin/index.js +1 -1
  95. package/lib/plugins/YouTubePlugin/index.js +1 -1
  96. package/lib/server/validation.js +1 -1
  97. package/lib/setupEnv.js +1 -1
  98. package/lib/themes/CommentEditorTheme.js +1 -1
  99. package/lib/themes/PlaygroundEditorTheme.js +1 -1
  100. package/lib/themes/StickyEditorTheme.js +1 -1
  101. package/lib/tyes.dt.js +1 -1
  102. package/lib/utils/docSerialization.js +1 -1
  103. package/lib/utils/emoji-list.js +1 -1
  104. package/lib/utils/getDOMRangeRect.js +1 -1
  105. package/lib/utils/getSelectedNode.js +1 -1
  106. package/lib/utils/getThemeSelector.js +1 -1
  107. package/lib/utils/isMobileWidth.js +1 -1
  108. package/lib/utils/joinClasses.js +1 -1
  109. package/lib/utils/setFloatingElemPosition.js +1 -1
  110. package/lib/utils/setFloatingElemPositionForLinkEditor.js +1 -1
  111. package/lib/utils/swipe.js +1 -1
  112. package/lib/utils/url.js +1 -1
  113. package/package.json +4 -4
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { LoroDoc, EphemeralStore } from 'loro-crdt';
@@ -20,13 +20,13 @@ const persistenceDir = process.env.YPERSISTENCE;
20
20
  // Helper function to log tree structure for debugging
21
21
  const logTreeStructure = (doc, context) => {
22
22
  try {
23
- console.log(`[Server] ${context} - Tree Structure Debug:`);
23
+ console.log(`[LORO] ${context} - Tree Structure Debug:`);
24
24
  // Try to get the tree container using getTree method
25
25
  try {
26
26
  const tree = doc.getTree('tree');
27
27
  if (tree) {
28
28
  const nodes = tree.nodes();
29
- console.log(`[Server] Total nodes in tree: ${nodes.length}`);
29
+ console.log(`[LORO] Total nodes in tree: ${nodes.length}`);
30
30
  // Helper function to recursively log tree structure
31
31
  const logTreeStructureRecursive = (node, prefix = '', isLast = true, depth = 0) => {
32
32
  const data = Object.fromEntries(node.data.entries());
@@ -34,7 +34,7 @@ const logTreeStructure = (doc, context) => {
34
34
  const elementType = data.elementType || 'no-type';
35
35
  const connector = depth === 0 ? '' : (isLast ? '└── ' : '├── ');
36
36
  const nodeInfo = `TreeID(${treeId.slice(0, 8)}...) [${elementType}]`;
37
- console.log(`[Server] ${prefix}${connector}${nodeInfo}`);
37
+ console.log(`[LORO] ${prefix}${connector}${nodeInfo}`);
38
38
  const children = node.children();
39
39
  if (children && children.length > 0) {
40
40
  children.forEach((child, index) => {
@@ -50,10 +50,10 @@ const logTreeStructure = (doc, context) => {
50
50
  const data = Object.fromEntries(node.data.entries());
51
51
  return !parent || data.isRoot;
52
52
  });
53
- console.log(`[Server] Root nodes: ${rootNodes.length}`);
53
+ console.log(`[LORO] Root nodes: ${rootNodes.length}`);
54
54
  console.log('');
55
55
  if (rootNodes.length === 0) {
56
- console.log('[Server] ⚠️ No root nodes found!');
56
+ console.log('[LORO] No root nodes found!');
57
57
  }
58
58
  else {
59
59
  rootNodes.forEach((root, index) => {
@@ -63,25 +63,25 @@ const logTreeStructure = (doc, context) => {
63
63
  }
64
64
  }
65
65
  else {
66
- console.log(`[Server] No tree container found`);
66
+ console.log(`[LORO] No tree container found`);
67
67
  }
68
68
  }
69
69
  catch (treeError) {
70
- console.log(`[Server] Error accessing tree container:`, treeError.message);
70
+ console.log(`[LORO] Error accessing tree container:`, treeError.message);
71
71
  }
72
72
  // Try export with different modes for debugging
73
73
  try {
74
74
  const snapshot = doc.export({ mode: 'snapshot' });
75
- console.log(`[Server] Snapshot size: ${snapshot.length} bytes`);
75
+ console.log(`[LORO] Snapshot size: ${snapshot.length} bytes`);
76
76
  const update = doc.export({ mode: 'update' });
77
- console.log(`[Server] Update size: ${update.length} bytes`);
77
+ console.log(`[LORO] Update size: ${update.length} bytes`);
78
78
  }
79
79
  catch (exportError) {
80
- console.log(`[Server] Error exporting:`, exportError.message);
80
+ console.log(`[LORO] Error exporting:`, exportError.message);
81
81
  }
82
82
  }
83
83
  catch (error) {
84
- console.warn(`[Server] Error logging tree structure:`, error);
84
+ console.warn(`[LORO] Error logging tree structure:`, error);
85
85
  }
86
86
  };
87
87
  /**
@@ -193,7 +193,7 @@ const messageListener = (conn, doc, message) => {
193
193
  messageStr = decoder.decode(message);
194
194
  }
195
195
  catch (decodeError) {
196
- console.warn(`[Server] messageListener - Failed to decode ArrayBuffer as string, treating as binary Loro update`);
196
+ console.warn(`[LORO] messageListener - Failed to decode ArrayBuffer as string, treating as binary Loro update`);
197
197
  // If decoding fails, treat as raw binary Loro update
198
198
  const updateBytes = new Uint8Array(message);
199
199
  doc.doc.import(updateBytes);
@@ -212,7 +212,7 @@ const messageListener = (conn, doc, message) => {
212
212
  messageStr = decoder.decode(message);
213
213
  }
214
214
  catch (decodeError) {
215
- console.warn(`[Server] messageListener - Failed to decode Uint8Array as string, treating as binary Loro update`);
215
+ console.warn(`[LORO] messageListener - Failed to decode Uint8Array as string, treating as binary Loro update`);
216
216
  // If decoding fails, treat as raw binary Loro update
217
217
  doc.doc.import(message);
218
218
  // Broadcast the update to other connections
@@ -225,7 +225,7 @@ const messageListener = (conn, doc, message) => {
225
225
  }
226
226
  }
227
227
  else {
228
- console.warn(`[Server] messageListener - Unknown message type:`, typeof message);
228
+ console.warn(`[LORO] messageListener - Unknown message type:`, typeof message);
229
229
  return;
230
230
  }
231
231
  if (!messageStr || messageStr.length === 0) {
@@ -235,31 +235,31 @@ const messageListener = (conn, doc, message) => {
235
235
  messageData = JSON.parse(messageStr);
236
236
  }
237
237
  catch (parseError) {
238
- console.warn(`[Server] messageListener - JSON parse error:`, parseError.message);
239
- console.warn(`[Server] messageListener - Raw message:`, messageStr.substring(0, 500));
238
+ console.warn(`[LORO] messageListener - JSON parse error:`, parseError.message);
239
+ console.warn(`[LORO] messageListener - Raw message:`, messageStr.substring(0, 500));
240
240
  return;
241
241
  }
242
- console.log(`[Server] Received message type: ${messageData.type} for doc: ${doc.name}`);
242
+ console.log(`[LORO] Received message type: ${messageData.type} for doc: ${doc.name}`);
243
243
  switch (messageData.type) {
244
244
  case messageQuerySnapshot:
245
245
  // Client is requesting a snapshot - send current document state
246
246
  const requestId = Math.random().toString(36).substr(2, 9);
247
- console.log(`[Server] Client requesting snapshot for doc: ${doc.name} (Request ID: ${requestId})`);
247
+ console.log(`[LORO] Client requesting snapshot for doc: ${doc.name} (Request ID: ${requestId})`);
248
248
  // Log tree structure before creating snapshot
249
249
  // logTreeStructure(doc.doc, `Before creating snapshot (Request ID: ${requestId})`)
250
250
  try {
251
251
  const snapshot = doc.doc.export({ mode: 'snapshot' });
252
- console.log(`[Server] Sending snapshot response: ${snapshot.length} bytes (Request ID: ${requestId})`);
252
+ console.log(`[LORO] Sending snapshot response: ${snapshot.length} bytes (Request ID: ${requestId})`);
253
253
  // Verify the snapshot contains expected content
254
254
  const tree = doc.doc.getTree('tree');
255
255
  const nodes = tree.nodes();
256
- console.log(`[Server] Snapshot contains ${nodes.length} nodes from server document`);
256
+ console.log(`[LORO] Snapshot contains ${nodes.length} nodes from server document`);
257
257
  // Send binary snapshot data directly instead of wrapped message
258
258
  conn.send(snapshot);
259
259
  }
260
260
  catch (snapshotError) {
261
- console.error(`[Server] ERROR creating/sending snapshot:`, snapshotError.message);
262
- console.error(`[Server] Stack:`, snapshotError.stack);
261
+ console.error(`[LORO] ERROR creating/sending snapshot:`, snapshotError.message);
262
+ console.error(`[LORO] Stack:`, snapshotError.stack);
263
263
  }
264
264
  break;
265
265
  case messageEphemeral:
@@ -273,7 +273,7 @@ const messageListener = (conn, doc, message) => {
273
273
  doc.ephemeralStore.apply(ephemeralBytes);
274
274
  const afterStates = doc.ephemeralStore.getAllStates();
275
275
  const afterKeys = Object.keys(afterStates);
276
- console.log(`📡 SERVER DEBUG - Applied ephemeral update from ${conn.id}:`, {
276
+ console.log(` SERVER DEBUG - Applied ephemeral update from ${conn.id}:`, {
277
277
  bytesLength: ephemeralBytes.length,
278
278
  beforeKeys,
279
279
  afterKeys,
@@ -293,7 +293,7 @@ const messageListener = (conn, doc, message) => {
293
293
  const allStates = doc.ephemeralStore.getAllStates();
294
294
  const allKeys = Object.keys(allStates);
295
295
  const ephemeralUpdate = doc.ephemeralStore.encodeAll();
296
- console.log(`📡 SERVER DEBUG - Client ${conn.id} requesting ephemeral state:`, {
296
+ console.log(` SERVER DEBUG - Client ${conn.id} requesting ephemeral state:`, {
297
297
  allKeysAvailable: allKeys,
298
298
  encodedLength: ephemeralUpdate.length,
299
299
  totalConnections: doc.conns.size
@@ -306,7 +306,7 @@ const messageListener = (conn, doc, message) => {
306
306
  sendMessage(doc, conn, ephemeralResponse);
307
307
  }
308
308
  catch (error) {
309
- console.warn('[Server] messageQueryEphemeral - ERROR encoding/sending ephemeral state:', error);
309
+ console.warn('[LORO] messageQueryEphemeral - ERROR encoding/sending ephemeral state:', error);
310
310
  }
311
311
  break;
312
312
  case messageUpdate:
@@ -359,16 +359,16 @@ const closeConn = (doc, conn) => {
359
359
  if (doc.conns.size === 0) {
360
360
  if (persistence !== null) {
361
361
  // if persisted, we store state and cleanup document
362
- console.log(`[Server] Persisting document ${doc.name} before cleanup`);
362
+ console.log(`[LORO] Persisting document ${doc.name} before cleanup`);
363
363
  persistence.writeState(doc.name, doc).then(() => {
364
364
  // Cleanup WSSharedDoc resources (no destroy method needed for Loro)
365
- console.log(`[Server] Document ${doc.name} persisted and cleaned up`);
365
+ console.log(`[LORO] Document ${doc.name} persisted and cleaned up`);
366
366
  });
367
367
  docs.delete(doc.name);
368
368
  }
369
369
  else {
370
370
  // No persistence configured - keep document in memory for reconnections
371
- console.log(`[Server] No persistence configured - keeping document ${doc.name} in memory for future connections`);
371
+ console.log(`[LORO] No persistence configured - keeping document ${doc.name} in memory for future connections`);
372
372
  // logTreeStructure(doc.doc, `Document ${doc.name} structure before keeping in memory`)
373
373
  }
374
374
  }
@@ -425,17 +425,17 @@ export const setupWSConnection = (conn, req, { docName = (req.url || '').slice(1
425
425
  // logTreeStructure(doc.doc, `Before creating initial snapshot for new client ${conn.id}`)
426
426
  try {
427
427
  const snapshot = doc.doc.export({ mode: 'snapshot' });
428
- console.log(`[Server] Sending initial snapshot to new client: ${snapshot.length} bytes`);
428
+ console.log(`[LORO] Sending initial snapshot to new client: ${snapshot.length} bytes`);
429
429
  // Verify the snapshot contains expected content
430
430
  const tree = doc.doc.getTree('tree');
431
431
  const nodes = tree.nodes();
432
- console.log(`[Server] Initial snapshot contains ${nodes.length} nodes from server document`);
432
+ console.log(`[LORO] Initial snapshot contains ${nodes.length} nodes from server document`);
433
433
  // Send binary snapshot data directly instead of wrapped message
434
434
  conn.send(snapshot);
435
435
  }
436
436
  catch (snapshotError) {
437
- console.error(`[Server] ERROR creating/sending initial snapshot:`, snapshotError.message);
438
- console.error(`[Server] Stack:`, snapshotError.stack);
437
+ console.error(`[LORO] ERROR creating/sending initial snapshot:`, snapshotError.message);
438
+ console.error(`[LORO] Stack:`, snapshotError.stack);
439
439
  }
440
440
  // Send current ephemeral state if any
441
441
  const ephemeralUpdate = doc.ephemeralStore.encodeAll();
@@ -461,7 +461,7 @@ export class WSSharedDoc {
461
461
  this.name = name;
462
462
  this.doc = new LoroDoc();
463
463
  // Initialize the document with default Lexical content
464
- console.log(`[Server] Initializing document '${name}' with default content`);
464
+ console.log(`[LORO] Initializing document '${name}' with default content`);
465
465
  initializeLoroDocWithLexicalContent(this.doc);
466
466
  /**
467
467
  * Maps from conn to set of controlled ephemeral keys. Delete all keys when this conn is closed
@@ -503,7 +503,7 @@ export class WSSharedDoc {
503
503
  this.lastEphemeralSender = null;
504
504
  }
505
505
  catch (broadcastError) {
506
- console.warn(`[Server] ephemeralChangeIntegrator - ERROR broadcasting:`, {
506
+ console.warn(`[LORO] ephemeralChangeIntegrator - ERROR broadcasting:`, {
507
507
  error: broadcastError.message,
508
508
  stack: broadcastError.stack
509
509
  });
@@ -1,9 +1,37 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { $getNodeByKey, $getSelection, $isElementNode, $isLineBreakNode, $isRangeSelection, $isTextNode, } from 'lexical';
6
6
  import { createDOMRange, createRectsFromDOMRange } from '@lexical/selection';
7
+ function getDisplayNameFromAwareness(awareness) {
8
+ const awarenessData = awareness.awarenessData;
9
+ if (!awarenessData || typeof awarenessData !== 'object') {
10
+ return awareness.name;
11
+ }
12
+ const data = awarenessData;
13
+ const user = data.user;
14
+ const userRecord = user && typeof user === 'object' ? user : undefined;
15
+ const candidates = [
16
+ userRecord?.display_name,
17
+ userRecord?.displayName,
18
+ userRecord?.name,
19
+ userRecord?.username,
20
+ userRecord?.handle,
21
+ data.display_name,
22
+ data.displayName,
23
+ data.name,
24
+ data.username,
25
+ data.handle,
26
+ awareness.name,
27
+ ];
28
+ for (const candidate of candidates) {
29
+ if (typeof candidate === 'string' && candidate.trim().length > 0) {
30
+ return candidate;
31
+ }
32
+ }
33
+ return awareness.name;
34
+ }
7
35
  /*****************************************************************************/
8
36
  // Helper function to find Loro tree node for a Lexical NodeKey (read-only lookup)
9
37
  function findLoroTreeNodeForLexicalKey(nodeKey, binding) {
@@ -151,19 +179,8 @@ function createCollabCursor(name, color) {
151
179
  }
152
180
  function createCursorSelection(cursor, anchorKey, anchorOffset, focusKey, focusOffset, isCurrentUser = false) {
153
181
  const color = cursor.color;
154
- // Helper function to convert color to rgba with opacity
155
- const getColorWithOpacity = (color, opacity) => {
156
- if (color.startsWith('#')) {
157
- const hex = color.slice(1);
158
- const r = parseInt(hex.slice(0, 2), 16);
159
- const g = parseInt(hex.slice(2, 4), 16);
160
- const b = parseInt(hex.slice(4, 6), 16);
161
- return `rgba(${r}, ${g}, ${b}, ${opacity})`;
162
- }
163
- return color;
164
- };
165
- const caretColor = isCurrentUser ? getColorWithOpacity(color, 0.6) : color;
166
- const nameBackgroundColor = isCurrentUser ? getColorWithOpacity(color, 0.7) : color;
182
+ const caretColor = color;
183
+ const nameBackgroundColor = color;
167
184
  const caret = document.createElement('span');
168
185
  caret.style.cssText = `position:absolute;top:0;bottom:0;right:-1px;width:2px;background-color:${caretColor};z-index:10;${isCurrentUser ? 'opacity:0.8;' : ''}`;
169
186
  const name = document.createElement('span');
@@ -210,16 +227,25 @@ export function syncCursorPositions(binding, provider, options) {
210
227
  const awarenessState = awarenessStates[i];
211
228
  const [clientID, awareness] = awarenessState;
212
229
  visitedClientIDs.add(clientID);
213
- const { name, color, focusing } = awareness;
230
+ const { color } = awareness;
231
+ const displayName = getDisplayNameFromAwareness(awareness);
214
232
  const isCurrentUser = clientID === localClientID;
215
233
  let selection = null;
216
234
  let cursor = cursors.get(clientID);
217
235
  if (cursor === undefined) {
218
236
  // Add "(Me)" label for current user's cursor
219
- const cursorName = isCurrentUser ? `${name} (Me)` : name;
237
+ const cursorName = isCurrentUser ? `${displayName} (Me)` : displayName;
220
238
  cursor = createCollabCursor(cursorName, color);
221
239
  cursors.set(clientID, cursor);
222
240
  }
241
+ else if (cursor.name !== displayName) {
242
+ cursor.name = displayName;
243
+ if (cursor.selection) {
244
+ cursor.selection.name.textContent = isCurrentUser
245
+ ? `${displayName} (Me)`
246
+ : displayName;
247
+ }
248
+ }
223
249
  // Render cursor/selection whenever valid anchorPos/focusPos exist.
224
250
  // We intentionally do NOT gate on `focusing` because the FOCUS_COMMAND
225
251
  // and selection-update can race — the selection update may broadcast
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { RootNode, ElementNode, TextNode, LineBreakNode, DecoratorNode, $getSelection, $getNodeByKey } from 'lexical';
@@ -14,6 +14,13 @@ import { syncLexicalSelectionToLoro } from './SyncCursors';
14
14
  // import { syncCursorPositions, SyncCursorPositionsFn } from './SyncCursors';
15
15
  export function syncLexicalToLoro(binding, provider, update) {
16
16
  const { mutatedNodes, prevEditorState, editorState: currEditorState, } = update;
17
+ let __seedDebugMutationCount = 0;
18
+ if (mutatedNodes) {
19
+ mutatedNodes.forEach((nodeMap) => {
20
+ __seedDebugMutationCount += nodeMap.size;
21
+ });
22
+ }
23
+ console.log('[SEED-DEBUG] syncLexicalToLoro: mutatedNodes=', mutatedNodes ? 'map' : 'null', 'totalMutations=', __seedDebugMutationCount, 'tags=', Array.from(update.tags));
17
24
  // Process node mutations if present.
18
25
  // NOTE: mutatedNodes is null for selection-only updates (no DOM mutations).
19
26
  // See Lexical docs: "Will be null if no DOM was mutated, such as when only
@@ -53,50 +60,82 @@ export function syncLexicalToLoro(binding, provider, update) {
53
60
  });
54
61
  }
55
62
  });
56
- // Phase 2: Collect all ElementNode mutations, sort by depth, then propagate
57
- const elementMutations = [];
63
+ const collected = [];
58
64
  mutatedNodes.forEach((nodeMap, Klass) => {
59
- if (isClassExtending(Klass, ElementNode) && !isClassExtending(Klass, RootNode)) {
60
- nodeMap.forEach((mutation, nodeKey) => {
61
- // Compute depth in Lexical tree (root=0, paragraph=1, tablecell=3, etc.)
62
- let depth = 0;
63
- currEditorState.read(() => {
64
- const node = $getNodeByKey(nodeKey);
65
- if (node) {
66
- let current = node.getParent();
67
- while (current) {
68
- depth++;
69
- current = current.getParent();
70
- }
71
- }
72
- });
73
- elementMutations.push({ mutation, nodeKey, depth });
74
- });
65
+ let category = null;
66
+ if (isClassExtending(Klass, RootNode)) {
67
+ return; // handled in Phase 1
75
68
  }
76
- });
77
- // Sort by depth ascending → parents (depth 1) are propagated before children (depth 2, 3, …)
78
- elementMutations.sort((a, b) => a.depth - b.depth);
79
- for (const { mutation, nodeKey } of elementMutations) {
80
- propagateElementNode(update, mutation, nodeKey, mutatorOptions);
81
- }
82
- // Phase 3: Process leaf children (their parents are now guaranteed to be mapped)
83
- mutatedNodes.forEach((nodeMap, Klass) => {
84
- if (isClassExtending(Klass, TextNode)) {
85
- nodeMap.forEach((mutation, nodeKey) => {
86
- propagateTextNode(update, mutation, nodeKey, mutatorOptions);
87
- });
69
+ else if (isClassExtending(Klass, ElementNode)) {
70
+ category = 'element';
71
+ }
72
+ else if (isClassExtending(Klass, TextNode)) {
73
+ category = 'text';
88
74
  }
89
75
  else if (isClassExtending(Klass, LineBreakNode)) {
90
- nodeMap.forEach((mutation, nodeKey) => {
91
- propagateLineBreakNode(update, mutation, nodeKey, mutatorOptions);
92
- });
76
+ category = 'linebreak';
93
77
  }
94
78
  else if (isClassExtending(Klass, DecoratorNode)) {
95
- nodeMap.forEach((mutation, nodeKey) => {
96
- propagateDecoratorNode(update, mutation, nodeKey, mutatorOptions);
97
- });
79
+ category = 'decorator';
80
+ }
81
+ if (category === null) {
82
+ return;
98
83
  }
84
+ const resolvedCategory = category;
85
+ nodeMap.forEach((mutation, nodeKey) => {
86
+ // Compute depth (root=0, paragraph=1, inline element=2, …) and the
87
+ // node's index within its parent from the current editor state.
88
+ let depth = 0;
89
+ let index = 0;
90
+ currEditorState.read(() => {
91
+ const node = $getNodeByKey(nodeKey);
92
+ if (node) {
93
+ index = node.getIndexWithinParent();
94
+ let current = node.getParent();
95
+ while (current) {
96
+ depth++;
97
+ current = current.getParent();
98
+ }
99
+ }
100
+ });
101
+ collected.push({ mutation, nodeKey, depth, index, category: resolvedCategory });
102
+ });
99
103
  });
104
+ const dispatch = (info) => {
105
+ switch (info.category) {
106
+ case 'element':
107
+ propagateElementNode(update, info.mutation, info.nodeKey, mutatorOptions);
108
+ break;
109
+ case 'text':
110
+ propagateTextNode(update, info.mutation, info.nodeKey, mutatorOptions);
111
+ break;
112
+ case 'linebreak':
113
+ propagateLineBreakNode(update, info.mutation, info.nodeKey, mutatorOptions);
114
+ break;
115
+ case 'decorator':
116
+ propagateDecoratorNode(update, info.mutation, info.nodeKey, mutatorOptions);
117
+ break;
118
+ }
119
+ };
120
+ // Destroys first, deepest-first (children before parents), to avoid
121
+ // re-create/update races during bulk deletes.
122
+ collected
123
+ .filter(m => m.mutation === 'destroyed')
124
+ .sort((a, b) => b.depth - a.depth)
125
+ .forEach(dispatch);
126
+ // Creates next, shallow-first then ascending sibling index (see rationale
127
+ // above) so every Loro insertion index stays within range.
128
+ collected
129
+ .filter(m => m.mutation === 'created')
130
+ .sort((a, b) => a.depth - b.depth || a.index - b.index)
131
+ .forEach(dispatch);
132
+ // Updates last, parents-first for determinism. Updates never call
133
+ // createLoroNode (the mapping already exists) so their order is not
134
+ // index-sensitive.
135
+ collected
136
+ .filter(m => m.mutation === 'updated')
137
+ .sort((a, b) => a.depth - b.depth || a.index - b.index)
138
+ .forEach(dispatch);
100
139
  // Commit only when there were actual node mutations (not selection-only changes)
101
140
  binding.doc.commit({ origin: binding.doc.peerIdStr });
102
141
  }
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { $addUpdateTag, $createParagraphNode, $getRoot, $getSelection, $isRangeSelection, $setSelection, SKIP_COLLAB_TAG, SKIP_SCROLL_INTO_VIEW_TAG } from 'lexical';
@@ -30,31 +30,37 @@ export function syncLoroToLexical(binding, provider, eventBatch, isFromUndoMange
30
30
  }
31
31
  // Process Loro events and apply them to Lexical using the appropriate integrators
32
32
  eventBatch.events.forEach((event, index) => {
33
- switch (event.diff.type) {
34
- case 'tree':
35
- // Call internal method that doesn't wrap in editor.update()
36
- treeIntegrator.integrateInternal(event.diff, binding, provider);
37
- break;
38
- case 'map':
39
- // Call internal method that doesn't wrap in editor.update()
40
- if (event.target) {
41
- mapIntegrator.integrateWithContextInternal(event.diff, event.target, binding, provider);
42
- }
43
- else {
44
- mapIntegrator.integrateInternal(event.diff, binding, provider);
45
- }
46
- break;
47
- case 'list':
48
- listIntegrator.integrateInternal(event.diff, binding, provider);
49
- break;
50
- case 'text':
51
- textIntegrator.integrateInternal(event.diff, binding, provider);
52
- break;
53
- case 'counter':
54
- counterIntegrator.integrateInternal(event.diff, binding, provider);
55
- break;
56
- default:
57
- throw new Error(`Unsupported event diff type: ${event.diff.type}. Supported types are: 'tree', 'map', 'list', 'text', 'counter'.`);
33
+ try {
34
+ switch (event.diff.type) {
35
+ case 'tree':
36
+ // Call internal method that doesn't wrap in editor.update()
37
+ treeIntegrator.integrateInternal(event.diff, binding, provider);
38
+ break;
39
+ case 'map':
40
+ // Call internal method that doesn't wrap in editor.update()
41
+ if (event.target) {
42
+ mapIntegrator.integrateWithContextInternal(event.diff, event.target, binding, provider);
43
+ }
44
+ else {
45
+ mapIntegrator.integrateInternal(event.diff, binding, provider);
46
+ }
47
+ break;
48
+ case 'list':
49
+ listIntegrator.integrateInternal(event.diff, binding, provider);
50
+ break;
51
+ case 'text':
52
+ textIntegrator.integrateInternal(event.diff, binding, provider);
53
+ break;
54
+ case 'counter':
55
+ counterIntegrator.integrateInternal(event.diff, binding, provider);
56
+ break;
57
+ default:
58
+ throw new Error(`Unsupported event diff type: ${event.diff.type}. Supported types are: 'tree', 'map', 'list', 'text', 'counter'.`);
59
+ }
60
+ }
61
+ catch (err) {
62
+ console.error('[SEED-DEBUG] syncLoroToLexical: integrator threw for event', index, 'diffType=', event.diff?.type, 'target=', String(event?.target), 'diff=', JSON.stringify(event.diff), err);
63
+ throw err;
58
64
  }
59
65
  });
60
66
  const selection = $getSelection();
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  /**