@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.
- package/README.md +2 -2
- package/lib/Editor.js +3 -2
- package/lib/appSettings.js +1 -1
- package/lib/collab/loro/Bindings.js +7 -7
- package/lib/collab/loro/Debug.js +35 -35
- package/lib/collab/loro/LexicalCollaborationContext.js +1 -1
- package/lib/collab/loro/LexicalCollaborationPlugin.d.ts +6 -1
- package/lib/collab/loro/LexicalCollaborationPlugin.js +41 -13
- package/lib/collab/loro/State.js +1 -1
- package/lib/collab/loro/components/LoroCollaborators.js +7 -19
- package/lib/collab/loro/components/index.js +1 -1
- package/lib/collab/loro/index.js +1 -1
- package/lib/collab/loro/integrators/BaseIntegrator.js +1 -1
- package/lib/collab/loro/integrators/CounterIntegrator.js +1 -1
- package/lib/collab/loro/integrators/ListIntegrator.js +4 -4
- package/lib/collab/loro/integrators/MapIntegrator.js +4 -4
- package/lib/collab/loro/integrators/TextIntegrator.js +2 -2
- package/lib/collab/loro/integrators/TreeIntegrator.d.ts +5 -0
- package/lib/collab/loro/integrators/TreeIntegrator.js +184 -142
- package/lib/collab/loro/nodes/NodeFactory.js +26 -47
- package/lib/collab/loro/nodes/NodesMapper.js +19 -7
- package/lib/collab/loro/propagators/DecoratorNodePropagator.js +1 -1
- package/lib/collab/loro/propagators/ElementNodePropagator.d.ts +1 -1
- package/lib/collab/loro/propagators/ElementNodePropagator.js +102 -35
- package/lib/collab/loro/propagators/LineBreakNodePropagator.js +1 -1
- package/lib/collab/loro/propagators/RootNodePropagator.js +1 -1
- package/lib/collab/loro/propagators/TextNodePropagator.js +55 -45
- package/lib/collab/loro/propagators/index.d.ts +1 -1
- package/lib/collab/loro/propagators/index.js +1 -1
- package/lib/collab/loro/provider/websocket.d.ts +1 -0
- package/lib/collab/loro/provider/websocket.js +47 -12
- package/lib/collab/loro/servers/index.js +1 -1
- package/lib/collab/loro/servers/ws/callback.js +1 -1
- package/lib/collab/loro/servers/ws/server.js +1 -1
- package/lib/collab/loro/servers/ws/utils.js +35 -35
- package/lib/collab/loro/sync/SyncCursors.js +42 -16
- package/lib/collab/loro/sync/SyncLexicalToLoro.js +76 -37
- package/lib/collab/loro/sync/SyncLoroToLexical.js +32 -26
- package/lib/collab/loro/types/LexicalNodeData.js +1 -1
- package/lib/collab/loro/useCollaboration.js +173 -28
- package/lib/collab/loro/utils/InitialContent.js +1 -1
- package/lib/collab/loro/utils/Invariant.d.ts +15 -0
- package/lib/collab/loro/utils/Invariant.js +24 -0
- package/lib/collab/loro/utils/LexicalToLoro.js +1 -1
- package/lib/collab/loro/utils/Utils.js +1 -1
- package/lib/collab/loro/wsProvider.js +21 -4
- package/lib/collab/utils/invariant.js +1 -1
- package/lib/collab/utils/simpleDiffWithCursor.js +1 -1
- package/lib/commenting/index.js +1 -1
- package/lib/demo.js +1 -1
- package/lib/hooks/useFlashMessage.js +1 -1
- package/lib/hooks/useReport.js +1 -1
- package/lib/index.js +1 -1
- package/lib/nodes/AutocompleteNode.js +1 -1
- package/lib/nodes/EmojiNode.js +1 -1
- package/lib/nodes/KeywordNode.js +1 -1
- package/lib/nodes/LayoutContainerNode.js +1 -1
- package/lib/nodes/LayoutItemNode.js +1 -1
- package/lib/nodes/MentionNode.js +1 -1
- package/lib/nodes/PlaygroundNodes.js +1 -1
- package/lib/nodes/SpecialTextNode.js +1 -1
- package/lib/plugins/AutocompletePlugin/index.js +1 -1
- package/lib/plugins/CodeActionMenuPlugin/components/CopyButton/index.js +1 -1
- package/lib/plugins/CodeActionMenuPlugin/components/PrettierButton/index.js +1 -1
- package/lib/plugins/CodeActionMenuPlugin/utils.js +1 -1
- package/lib/plugins/CodeHighlightPrismPlugin/index.js +1 -1
- package/lib/plugins/CodeHighlightShikiPlugin/index.js +1 -1
- package/lib/plugins/CollapsiblePlugin/CollapsibleContainerNode.js +1 -1
- package/lib/plugins/CollapsiblePlugin/CollapsibleContentNode.js +1 -1
- package/lib/plugins/CollapsiblePlugin/CollapsibleTitleNode.js +1 -1
- package/lib/plugins/CollapsiblePlugin/CollapsibleUtils.js +1 -1
- package/lib/plugins/CollapsiblePlugin/index.js +1 -1
- package/lib/plugins/CounterPlugin/index.js +1 -1
- package/lib/plugins/DateTimePlugin/index.js +1 -1
- package/lib/plugins/DebugPlugin/index.js +59 -3
- package/lib/plugins/DragDropPastePlugin/index.js +1 -1
- package/lib/plugins/EmojiPickerPlugin/index.js +1 -1
- package/lib/plugins/EmojisPlugin/index.js +1 -1
- package/lib/plugins/FigmaPlugin/index.js +1 -1
- package/lib/plugins/KeywordsPlugin/index.js +1 -1
- package/lib/plugins/LayoutPlugin/LayoutPlugin.js +1 -1
- package/lib/plugins/MarkdownTransformers/index.js +1 -1
- package/lib/plugins/MaxLengthPlugin/index.js +1 -1
- package/lib/plugins/PageBreakPlugin/index.js +1 -1
- package/lib/plugins/ShortcutsPlugin/index.js +1 -1
- package/lib/plugins/ShortcutsPlugin/shortcuts.js +1 -1
- package/lib/plugins/SpecialTextPlugin/index.js +1 -1
- package/lib/plugins/SpeechToTextPlugin/index.js +1 -1
- package/lib/plugins/StickyPlugin/index.js +1 -1
- package/lib/plugins/TabFocusPlugin/index.js +1 -1
- package/lib/plugins/ToolbarPlugin/fontSize.js +1 -1
- package/lib/plugins/ToolbarPlugin/utils.js +1 -1
- package/lib/plugins/TwitterPlugin/index.js +1 -1
- package/lib/plugins/TypingPerfPlugin/index.js +1 -1
- package/lib/plugins/YouTubePlugin/index.js +1 -1
- package/lib/server/validation.js +1 -1
- package/lib/setupEnv.js +1 -1
- package/lib/themes/CommentEditorTheme.js +1 -1
- package/lib/themes/PlaygroundEditorTheme.js +1 -1
- package/lib/themes/StickyEditorTheme.js +1 -1
- package/lib/tyes.dt.js +1 -1
- package/lib/utils/docSerialization.js +1 -1
- package/lib/utils/emoji-list.js +1 -1
- package/lib/utils/getDOMRangeRect.js +1 -1
- package/lib/utils/getSelectedNode.js +1 -1
- package/lib/utils/getThemeSelector.js +1 -1
- package/lib/utils/isMobileWidth.js +1 -1
- package/lib/utils/joinClasses.js +1 -1
- package/lib/utils/setFloatingElemPosition.js +1 -1
- package/lib/utils/setFloatingElemPositionForLinkEditor.js +1 -1
- package/lib/utils/swipe.js +1 -1
- package/lib/utils/url.js +1 -1
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright (c)
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
53
|
+
console.log(`[LORO] Root nodes: ${rootNodes.length}`);
|
|
54
54
|
console.log('');
|
|
55
55
|
if (rootNodes.length === 0) {
|
|
56
|
-
console.log('[
|
|
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(`[
|
|
66
|
+
console.log(`[LORO] No tree container found`);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
catch (treeError) {
|
|
70
|
-
console.log(`[
|
|
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(`[
|
|
75
|
+
console.log(`[LORO] Snapshot size: ${snapshot.length} bytes`);
|
|
76
76
|
const update = doc.export({ mode: 'update' });
|
|
77
|
-
console.log(`[
|
|
77
|
+
console.log(`[LORO] Update size: ${update.length} bytes`);
|
|
78
78
|
}
|
|
79
79
|
catch (exportError) {
|
|
80
|
-
console.log(`[
|
|
80
|
+
console.log(`[LORO] Error exporting:`, exportError.message);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
catch (error) {
|
|
84
|
-
console.warn(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
239
|
-
console.warn(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
262
|
-
console.error(`[
|
|
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(
|
|
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(
|
|
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('[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
438
|
-
console.error(`[
|
|
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(`[
|
|
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(`[
|
|
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)
|
|
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
|
-
|
|
155
|
-
const
|
|
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 {
|
|
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 ? `${
|
|
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)
|
|
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
|
-
|
|
57
|
-
const elementMutations = [];
|
|
63
|
+
const collected = [];
|
|
58
64
|
mutatedNodes.forEach((nodeMap, Klass) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
91
|
-
propagateLineBreakNode(update, mutation, nodeKey, mutatorOptions);
|
|
92
|
-
});
|
|
76
|
+
category = 'linebreak';
|
|
93
77
|
}
|
|
94
78
|
else if (isClassExtending(Klass, DecoratorNode)) {
|
|
95
|
-
|
|
96
|
-
|
|
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)
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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();
|