@pm-cm/yjs 0.0.3 → 0.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/dist/index.cjs +177 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +178 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -171,7 +171,13 @@ function createYjsBridge(config, options) {
|
|
|
171
171
|
return { source: "initial", parseError: true };
|
|
172
172
|
}
|
|
173
173
|
const pmDoc = (0, import_y_prosemirror.yXmlFragmentToProseMirrorRootNode)(sharedProseMirror, schema);
|
|
174
|
-
|
|
174
|
+
let canonicalText;
|
|
175
|
+
try {
|
|
176
|
+
canonicalText = serialize(pmDoc);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
onError({ code: "serialize-error", message: "failed to serialize ProseMirror document during bootstrap", cause: error });
|
|
179
|
+
return { source: "initial", parseError: true };
|
|
180
|
+
}
|
|
175
181
|
replaceSharedText(sharedText, canonicalText, ORIGIN_INIT, normalize);
|
|
176
182
|
lastBridgedText = normalize(canonicalText);
|
|
177
183
|
return { source: "initial" };
|
|
@@ -233,7 +239,13 @@ function createYjsBridge(config, options) {
|
|
|
233
239
|
return {
|
|
234
240
|
bootstrapResult,
|
|
235
241
|
syncToSharedText(doc2) {
|
|
236
|
-
|
|
242
|
+
let text;
|
|
243
|
+
try {
|
|
244
|
+
text = serialize(doc2);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
onError({ code: "serialize-error", message: "failed to serialize ProseMirror document", cause: error });
|
|
247
|
+
return { ok: false, reason: "serialize-error" };
|
|
248
|
+
}
|
|
237
249
|
const result = replaceSharedText(sharedText, text, ORIGIN_PM_TO_TEXT, normalize);
|
|
238
250
|
if (result.ok || result.reason === "unchanged") {
|
|
239
251
|
lastBridgedText = normalize(text);
|
|
@@ -271,10 +283,12 @@ function createAwarenessProxy(awareness, cursorField = "pmCursor") {
|
|
|
271
283
|
const states = target.getStates();
|
|
272
284
|
const remapped = /* @__PURE__ */ new Map();
|
|
273
285
|
states.forEach((state, clientId) => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
cursor:
|
|
277
|
-
}
|
|
286
|
+
const s = state;
|
|
287
|
+
if (cursorField in s) {
|
|
288
|
+
remapped.set(clientId, { ...s, cursor: s[cursorField] ?? null });
|
|
289
|
+
} else {
|
|
290
|
+
remapped.set(clientId, s);
|
|
291
|
+
}
|
|
278
292
|
});
|
|
279
293
|
return remapped;
|
|
280
294
|
};
|
|
@@ -291,14 +305,12 @@ var import_y_prosemirror3 = require("y-prosemirror");
|
|
|
291
305
|
// src/bridge-sync-plugin.ts
|
|
292
306
|
var import_prosemirror_state = require("prosemirror-state");
|
|
293
307
|
var bridgeSyncPluginKey = new import_prosemirror_state.PluginKey("pm-cm-bridge-sync");
|
|
294
|
-
var wiredBridges = /* @__PURE__ */ new
|
|
308
|
+
var wiredBridges = /* @__PURE__ */ new WeakMap();
|
|
295
309
|
var defaultOnWarning = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`);
|
|
296
310
|
function createBridgeSyncPlugin(bridge, options = {}) {
|
|
297
311
|
const warn = options.onWarning ?? defaultOnWarning;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
wiredBridges.add(bridge);
|
|
312
|
+
let yjsBatchSeen = false;
|
|
313
|
+
let needsSync = false;
|
|
302
314
|
return new import_prosemirror_state.Plugin({
|
|
303
315
|
key: bridgeSyncPluginKey,
|
|
304
316
|
state: {
|
|
@@ -306,27 +318,44 @@ function createBridgeSyncPlugin(bridge, options = {}) {
|
|
|
306
318
|
return { needsSync: false };
|
|
307
319
|
},
|
|
308
320
|
apply(tr, _prev) {
|
|
309
|
-
if (!tr.docChanged) return { needsSync
|
|
310
|
-
if (bridge.isYjsSyncChange(tr))
|
|
321
|
+
if (!tr.docChanged) return { needsSync };
|
|
322
|
+
if (bridge.isYjsSyncChange(tr)) {
|
|
323
|
+
yjsBatchSeen = true;
|
|
324
|
+
needsSync = false;
|
|
325
|
+
return { needsSync: false };
|
|
326
|
+
}
|
|
327
|
+
if (yjsBatchSeen) {
|
|
328
|
+
needsSync = false;
|
|
329
|
+
return { needsSync: false };
|
|
330
|
+
}
|
|
331
|
+
needsSync = true;
|
|
311
332
|
return { needsSync: true };
|
|
312
333
|
}
|
|
313
334
|
},
|
|
314
335
|
view() {
|
|
336
|
+
const count = wiredBridges.get(bridge) ?? 0;
|
|
337
|
+
if (count > 0) {
|
|
338
|
+
warn({ code: "bridge-already-wired", message: "this bridge is already wired to another plugin instance" });
|
|
339
|
+
}
|
|
340
|
+
wiredBridges.set(bridge, count + 1);
|
|
315
341
|
return {
|
|
316
342
|
update(view) {
|
|
317
|
-
|
|
318
|
-
if (state?.needsSync) {
|
|
343
|
+
if (needsSync) {
|
|
319
344
|
const result = bridge.syncToSharedText(view.state.doc);
|
|
320
345
|
if (!result.ok) {
|
|
321
|
-
if (result.reason
|
|
346
|
+
if (result.reason !== "unchanged") {
|
|
322
347
|
options.onSyncFailure?.(result, view);
|
|
323
348
|
warn({ code: "sync-failed", message: `bridge sync failed: ${result.reason}` });
|
|
324
349
|
}
|
|
325
350
|
}
|
|
326
351
|
}
|
|
352
|
+
needsSync = false;
|
|
353
|
+
yjsBatchSeen = false;
|
|
327
354
|
},
|
|
328
355
|
destroy() {
|
|
329
|
-
wiredBridges.
|
|
356
|
+
const remaining = (wiredBridges.get(bridge) ?? 1) - 1;
|
|
357
|
+
if (remaining <= 0) wiredBridges.delete(bridge);
|
|
358
|
+
else wiredBridges.set(bridge, remaining);
|
|
330
359
|
}
|
|
331
360
|
};
|
|
332
361
|
}
|
|
@@ -377,12 +406,23 @@ function createCursorSyncPlugin(options) {
|
|
|
377
406
|
const warn = options.onWarning ?? defaultOnWarning2;
|
|
378
407
|
const cursorFieldName = options.cursorFieldName ?? "pmCursor";
|
|
379
408
|
const cmCursorFieldName = options.cmCursorFieldName ?? "cursor";
|
|
409
|
+
if (sharedText && cursorFieldName === cmCursorFieldName) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`createCursorSyncPlugin: cursorFieldName and cmCursorFieldName must differ when sharedText is provided (both are "${cursorFieldName}")`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
380
414
|
let warnedSyncPluginMissing = false;
|
|
415
|
+
let pendingCmCursor = null;
|
|
381
416
|
let cachedMap = null;
|
|
382
417
|
let cachedMapDoc = null;
|
|
383
418
|
function getOrBuildMap(doc) {
|
|
384
419
|
if (cachedMapDoc !== doc || !cachedMap) {
|
|
385
|
-
|
|
420
|
+
try {
|
|
421
|
+
cachedMap = (0, import_core.buildCursorMap)(doc, serialize);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
warn({ code: "cursor-map-error", message: "failed to build cursor map \u2014 cursor sync skipped" });
|
|
424
|
+
cachedMap = null;
|
|
425
|
+
}
|
|
386
426
|
cachedMapDoc = doc;
|
|
387
427
|
}
|
|
388
428
|
return cachedMap;
|
|
@@ -396,62 +436,141 @@ function createCursorSyncPlugin(options) {
|
|
|
396
436
|
apply(tr, prev, _oldState, newState) {
|
|
397
437
|
const cmMeta = tr.getMeta(cursorSyncPluginKey);
|
|
398
438
|
if (cmMeta) {
|
|
399
|
-
|
|
439
|
+
pendingCmCursor = cmMeta;
|
|
400
440
|
}
|
|
401
441
|
let mappedTextOffset = prev.mappedTextOffset;
|
|
402
442
|
if (tr.selectionSet || tr.docChanged) {
|
|
403
443
|
const map = getOrBuildMap(newState.doc);
|
|
404
|
-
mappedTextOffset = (0, import_core.cursorMapLookup)(map, newState.selection.anchor);
|
|
444
|
+
mappedTextOffset = map ? (0, import_core.cursorMapLookup)(map, newState.selection.anchor) : null;
|
|
405
445
|
}
|
|
406
446
|
return {
|
|
407
|
-
pendingCm:
|
|
447
|
+
pendingCm: pendingCmCursor,
|
|
408
448
|
mappedTextOffset
|
|
409
449
|
};
|
|
410
450
|
}
|
|
411
451
|
},
|
|
412
|
-
view() {
|
|
452
|
+
view(editorView) {
|
|
453
|
+
let suppressCmReaction = false;
|
|
454
|
+
let lastCmAbsAnchor = -1;
|
|
455
|
+
let lastCmAbsHead = -1;
|
|
456
|
+
let cmCursorHandledByListener = false;
|
|
457
|
+
const handleAwarenessUpdate = ({ added, updated }) => {
|
|
458
|
+
if (suppressCmReaction) return;
|
|
459
|
+
if (!sharedText?.doc) return;
|
|
460
|
+
const localId = awareness.clientID;
|
|
461
|
+
if (!updated.includes(localId) && !added.includes(localId)) return;
|
|
462
|
+
const localState = awareness.getLocalState();
|
|
463
|
+
if (!localState) return;
|
|
464
|
+
const cmCursor = localState[cmCursorFieldName];
|
|
465
|
+
if (!cmCursor?.anchor || !cmCursor?.head) {
|
|
466
|
+
if (lastCmAbsAnchor !== -1) {
|
|
467
|
+
awareness.setLocalStateField(cursorFieldName, null);
|
|
468
|
+
lastCmAbsAnchor = -1;
|
|
469
|
+
lastCmAbsHead = -1;
|
|
470
|
+
}
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const absAnchor = (0, import_yjs.createAbsolutePositionFromRelativePosition)(cmCursor.anchor, sharedText.doc);
|
|
474
|
+
const absHead = (0, import_yjs.createAbsolutePositionFromRelativePosition)(cmCursor.head, sharedText.doc);
|
|
475
|
+
if (!absAnchor || !absHead) {
|
|
476
|
+
awareness.setLocalStateField(cursorFieldName, null);
|
|
477
|
+
lastCmAbsAnchor = -1;
|
|
478
|
+
lastCmAbsHead = -1;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (absAnchor.index === lastCmAbsAnchor && absHead.index === lastCmAbsHead) return;
|
|
482
|
+
const map = getOrBuildMap(editorView.state.doc);
|
|
483
|
+
if (!map) {
|
|
484
|
+
awareness.setLocalStateField(cursorFieldName, null);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const pmAnchor = (0, import_core.reverseCursorMapLookup)(map, absAnchor.index);
|
|
488
|
+
const pmHead = (0, import_core.reverseCursorMapLookup)(map, absHead.index);
|
|
489
|
+
if (pmAnchor === null || pmHead === null) {
|
|
490
|
+
awareness.setLocalStateField(cursorFieldName, null);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
lastCmAbsAnchor = absAnchor.index;
|
|
494
|
+
lastCmAbsHead = absHead.index;
|
|
495
|
+
cmCursorHandledByListener = true;
|
|
496
|
+
const ok = broadcastPmCursor(awareness, cursorFieldName, editorView, pmAnchor, pmHead);
|
|
497
|
+
if (!ok && !warnedSyncPluginMissing) {
|
|
498
|
+
warnedSyncPluginMissing = true;
|
|
499
|
+
warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
if (sharedText) {
|
|
503
|
+
awareness.on("update", handleAwarenessUpdate);
|
|
504
|
+
}
|
|
413
505
|
return {
|
|
414
506
|
update(view, prevState) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const pmHead = (0, import_core.reverseCursorMapLookup)(map, pluginState.pendingCm.head);
|
|
421
|
-
if (pmAnchor !== null && pmHead !== null) {
|
|
422
|
-
const ok = broadcastPmCursor(awareness, cursorFieldName, view, pmAnchor, pmHead);
|
|
423
|
-
if (!ok && !warnedSyncPluginMissing) {
|
|
424
|
-
warnedSyncPluginMissing = true;
|
|
425
|
-
warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
|
|
426
|
-
}
|
|
507
|
+
if (view.state.doc !== prevState.doc) {
|
|
508
|
+
lastCmAbsAnchor = -1;
|
|
509
|
+
lastCmAbsHead = -1;
|
|
510
|
+
if (sharedText?.doc && !suppressCmReaction) {
|
|
511
|
+
handleAwarenessUpdate({ added: [awareness.clientID], updated: [], removed: [] });
|
|
427
512
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
);
|
|
513
|
+
}
|
|
514
|
+
if (pendingCmCursor != null) {
|
|
515
|
+
const cursor = pendingCmCursor;
|
|
516
|
+
pendingCmCursor = null;
|
|
517
|
+
if (!cmCursorHandledByListener) {
|
|
518
|
+
const map = getOrBuildMap(view.state.doc);
|
|
519
|
+
const pmAnchor = map ? (0, import_core.reverseCursorMapLookup)(map, cursor.anchor) : null;
|
|
520
|
+
const pmHead = map ? (0, import_core.reverseCursorMapLookup)(map, cursor.head) : null;
|
|
521
|
+
if (pmAnchor !== null && pmHead !== null) {
|
|
522
|
+
const ok = broadcastPmCursor(awareness, cursorFieldName, view, pmAnchor, pmHead);
|
|
523
|
+
if (!ok && !warnedSyncPluginMissing) {
|
|
524
|
+
warnedSyncPluginMissing = true;
|
|
525
|
+
warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
|
|
526
|
+
}
|
|
527
|
+
if (sharedText?.doc) {
|
|
528
|
+
broadcastTextCursor(
|
|
529
|
+
awareness,
|
|
530
|
+
cmCursorFieldName,
|
|
531
|
+
sharedText,
|
|
532
|
+
cursor.anchor,
|
|
533
|
+
cursor.head
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
awareness.setLocalStateField(cursorFieldName, null);
|
|
538
|
+
}
|
|
436
539
|
}
|
|
540
|
+
cmCursorHandledByListener = false;
|
|
437
541
|
return;
|
|
438
542
|
}
|
|
439
543
|
if (view.hasFocus() && (view.state.selection !== prevState.selection || view.state.doc !== prevState.doc)) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
544
|
+
suppressCmReaction = true;
|
|
545
|
+
try {
|
|
546
|
+
const { anchor, head } = view.state.selection;
|
|
547
|
+
const ok = broadcastPmCursor(awareness, cursorFieldName, view, anchor, head);
|
|
548
|
+
if (!ok && !warnedSyncPluginMissing) {
|
|
549
|
+
warnedSyncPluginMissing = true;
|
|
550
|
+
warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
|
|
551
|
+
}
|
|
552
|
+
if (sharedText?.doc) {
|
|
553
|
+
const map = getOrBuildMap(view.state.doc);
|
|
554
|
+
const textAnchor = map ? (0, import_core.cursorMapLookup)(map, anchor) : null;
|
|
555
|
+
const textHead = map ? (0, import_core.cursorMapLookup)(map, head) : null;
|
|
556
|
+
if (textAnchor !== null && textHead !== null) {
|
|
557
|
+
broadcastTextCursor(awareness, cmCursorFieldName, sharedText, textAnchor, textHead);
|
|
558
|
+
} else {
|
|
559
|
+
awareness.setLocalStateField(cmCursorFieldName, null);
|
|
560
|
+
}
|
|
452
561
|
}
|
|
562
|
+
} finally {
|
|
563
|
+
suppressCmReaction = false;
|
|
453
564
|
}
|
|
454
565
|
}
|
|
566
|
+
cmCursorHandledByListener = false;
|
|
567
|
+
},
|
|
568
|
+
destroy() {
|
|
569
|
+
awareness.setLocalStateField(cursorFieldName, null);
|
|
570
|
+
if (sharedText) {
|
|
571
|
+
awareness.setLocalStateField(cmCursorFieldName, null);
|
|
572
|
+
awareness.off("update", handleAwarenessUpdate);
|
|
573
|
+
}
|
|
455
574
|
}
|
|
456
575
|
};
|
|
457
576
|
}
|
|
@@ -462,7 +581,10 @@ function syncCmCursor(view, anchor, head, onWarning) {
|
|
|
462
581
|
(onWarning ?? defaultOnWarning2)({ code: "cursor-sync-not-installed", message: "cursor sync plugin is not installed on this EditorView" });
|
|
463
582
|
return;
|
|
464
583
|
}
|
|
465
|
-
const sanitize = (v) =>
|
|
584
|
+
const sanitize = (v) => {
|
|
585
|
+
const n = Math.floor(v);
|
|
586
|
+
return Number.isFinite(n) ? Math.max(0, n) : 0;
|
|
587
|
+
};
|
|
466
588
|
view.dispatch(
|
|
467
589
|
view.state.tr.setMeta(cursorSyncPluginKey, {
|
|
468
590
|
anchor: sanitize(anchor),
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/bridge.ts","../src/types.ts","../src/awareness-proxy.ts","../src/collab-plugins.ts","../src/bridge-sync-plugin.ts","../src/cursor-sync-plugin.ts"],"sourcesContent":["export { createYjsBridge, replaceSharedText, replaceSharedProseMirror } from './bridge.js'\nexport type { YjsBridgeOptions, ReplaceResult, ReplaceTextResult, ReplaceProseMirrorResult } from './bridge.js'\nexport { createAwarenessProxy } from './awareness-proxy.js'\nexport { createCollabPlugins } from './collab-plugins.js'\nexport type { CollabPluginsOptions, ProseMirrorMapping, YCursorPluginOpts, YUndoPluginOpts } from './collab-plugins.js'\nexport {\n ORIGIN_TEXT_TO_PM,\n ORIGIN_PM_TO_TEXT,\n ORIGIN_INIT,\n} from './types.js'\nexport type {\n BootstrapResult,\n YjsBridgeConfig,\n YjsBridgeHandle,\n WarningCode,\n WarningEvent,\n OnWarning,\n} from './types.js'\n\n// Cursor mapping re-exported from @pm-cm/core\nexport { buildCursorMap, cursorMapLookup, reverseCursorMapLookup } from '@pm-cm/core'\nexport type { TextSegment, CursorMap, SerializeWithMap, CursorMapWriter, Matcher, MatchResult, MatchRun } from '@pm-cm/core'\n\n// Bridge sync plugin (auto PM→Y.Text wiring)\nexport { createBridgeSyncPlugin, bridgeSyncPluginKey } from './bridge-sync-plugin.js'\nexport type { BridgeSyncPluginOptions } from './bridge-sync-plugin.js'\n\n// Cursor sync plugin\nexport { createCursorSyncPlugin, cursorSyncPluginKey, syncCmCursor } from './cursor-sync-plugin.js'\nexport type { CursorSyncState, CursorSyncPluginOptions } from './cursor-sync-plugin.js'\n\n// Re-export types from @pm-cm/core\nexport { createCursorMapWriter, wrapSerialize } from '@pm-cm/core'\nexport type { Serialize, Parse, Normalize, OnError, ErrorCode, ErrorEvent } from '@pm-cm/core'\n","import type { Node } from 'prosemirror-model'\nimport type { Transaction } from 'prosemirror-state'\nimport { prosemirrorToYXmlFragment, yXmlFragmentToProseMirrorRootNode, ySyncPluginKey } from 'y-prosemirror'\nimport type { Doc, Text as YText, XmlFragment as YXmlFragment } from 'yjs'\nimport type { Normalize, OnError } from '@pm-cm/core'\nimport type { BootstrapResult, YjsBridgeConfig, YjsBridgeHandle } from './types.js'\nimport { ORIGIN_INIT, ORIGIN_TEXT_TO_PM, ORIGIN_PM_TO_TEXT } from './types.js'\n\nconst defaultNormalize: Normalize = (s) => s.replace(/\\r\\n?/g, '\\n')\nconst defaultOnError: OnError = (event) => console.error(`[bridge] ${event.code}: ${event.message}`, event.cause)\n\n/** Result of {@link replaceSharedText}. */\nexport type ReplaceTextResult =\n | { ok: true }\n | { ok: false; reason: 'unchanged' }\n | { ok: false; reason: 'detached' }\n\n/** Result of {@link replaceSharedProseMirror}. */\nexport type ReplaceProseMirrorResult =\n | { ok: true }\n | { ok: false; reason: 'parse-error' }\n | { ok: false; reason: 'detached' }\n\n/**\n * Union of all replace-result types. Kept for backward compatibility.\n * Prefer the narrower {@link ReplaceTextResult} / {@link ReplaceProseMirrorResult}.\n */\nexport type ReplaceResult = ReplaceTextResult | ReplaceProseMirrorResult\n\n/**\n * Replace `Y.Text` content using a minimal diff (common prefix/suffix trimming).\n * Returns a {@link ReplaceResult} indicating success or failure reason.\n */\nexport function replaceSharedText(\n sharedText: YText,\n next: string,\n origin: unknown,\n normalize: Normalize = defaultNormalize,\n): ReplaceTextResult {\n if (!sharedText.doc) {\n return { ok: false, reason: 'detached' }\n }\n\n const normalized = normalize(next)\n const current = sharedText.toString()\n if (current === normalized) {\n return { ok: false, reason: 'unchanged' }\n }\n\n // Minimal diff: find common prefix and suffix, replace only the changed middle.\n let start = 0\n const minLen = Math.min(current.length, normalized.length)\n while (start < minLen && current.charCodeAt(start) === normalized.charCodeAt(start)) {\n start++\n }\n\n let endCurrent = current.length\n let endNext = normalized.length\n while (endCurrent > start && endNext > start && current.charCodeAt(endCurrent - 1) === normalized.charCodeAt(endNext - 1)) {\n endCurrent--\n endNext--\n }\n\n sharedText.doc.transact(() => {\n const deleteCount = endCurrent - start\n if (deleteCount > 0) {\n sharedText.delete(start, deleteCount)\n }\n const insertStr = normalized.slice(start, endNext)\n if (insertStr.length > 0) {\n sharedText.insert(start, insertStr)\n }\n }, origin)\n\n return { ok: true }\n}\n\n/**\n * Replace `Y.XmlFragment` by parsing serialized text into a ProseMirror document.\n * Returns a {@link ReplaceResult} indicating success or failure reason.\n */\nexport function replaceSharedProseMirror(\n doc: Doc,\n fragment: YXmlFragment,\n text: string,\n origin: unknown,\n config: Pick<YjsBridgeConfig, 'schema' | 'parse' | 'normalize' | 'onError'>,\n): ReplaceProseMirrorResult {\n if (!fragment.doc) {\n return { ok: false, reason: 'detached' }\n }\n if (fragment.doc !== doc) {\n throw new Error('fragment belongs to a different Y.Doc than the provided doc')\n }\n\n const normalize = config.normalize ?? defaultNormalize\n const onError = config.onError ?? defaultOnError\n let nextDoc: Node\n try {\n nextDoc = config.parse(normalize(text), config.schema)\n } catch (error) {\n onError({ code: 'parse-error', message: 'failed to parse text into ProseMirror document', cause: error })\n return { ok: false, reason: 'parse-error' }\n }\n\n doc.transact(() => {\n prosemirrorToYXmlFragment(nextDoc, fragment)\n }, origin)\n return { ok: true }\n}\n\n/** Options for {@link createYjsBridge}. */\nexport type YjsBridgeOptions = {\n initialText?: string\n /** Which side wins when both sharedText and sharedProseMirror exist and differ. Default `'text'`. */\n prefer?: 'text' | 'prosemirror'\n}\n\n/**\n * Create a collaborative bridge that keeps `Y.Text` and `Y.XmlFragment` in sync.\n *\n * Runs a synchronous bootstrap to reconcile existing state, then installs a\n * `Y.Text` observer for the text → ProseMirror direction.\n *\n * @throws If `sharedText` or `sharedProseMirror` belong to a different `Y.Doc`.\n */\nexport function createYjsBridge(\n config: YjsBridgeConfig,\n options?: YjsBridgeOptions,\n): YjsBridgeHandle {\n const {\n doc,\n sharedText,\n sharedProseMirror,\n schema,\n serialize,\n parse,\n } = config\n const normalize = config.normalize ?? defaultNormalize\n const onError = config.onError ?? defaultOnError\n\n if (!sharedText.doc) {\n throw new Error('sharedText is not attached to any Y.Doc')\n }\n if (sharedText.doc !== doc) {\n throw new Error('sharedText belongs to a different Y.Doc than the provided doc')\n }\n if (!sharedProseMirror.doc) {\n throw new Error('sharedProseMirror is not attached to any Y.Doc')\n }\n if (sharedProseMirror.doc !== doc) {\n throw new Error('sharedProseMirror belongs to a different Y.Doc than the provided doc')\n }\n\n let lastBridgedText: string | null = null\n\n /** Returns `true` if the parse succeeded. */\n const syncTextToProsemirror = (origin: unknown): boolean => {\n const text = normalize(sharedText.toString())\n if (lastBridgedText === text) {\n return true\n }\n\n const result = replaceSharedProseMirror(doc, sharedProseMirror, text, origin, {\n schema,\n parse,\n normalize,\n onError,\n })\n if (result.ok) {\n lastBridgedText = text\n }\n return result.ok\n }\n\n const sharedProseMirrorToText = (fragment: YXmlFragment): string | null => {\n try {\n const pmDoc = yXmlFragmentToProseMirrorRootNode(fragment, schema)\n return normalize(serialize(pmDoc))\n } catch (error) {\n onError({ code: 'serialize-error', message: 'failed to convert ProseMirror fragment to text', cause: error })\n return null\n }\n }\n\n // Bootstrap\n const bootstrap = (): BootstrapResult => {\n const text = normalize(sharedText.toString())\n const hasText = text.length > 0\n const hasProsemirror = sharedProseMirror.length > 0\n\n if (!hasText && !hasProsemirror) {\n const initial = options?.initialText ?? ''\n if (initial.length > 0) {\n // Set Y.XmlFragment first, then derive Y.Text from serialize(parse(initial))\n // to ensure both shared types are in the same canonical form.\n const initResult = replaceSharedProseMirror(doc, sharedProseMirror, initial, ORIGIN_INIT, {\n schema,\n parse,\n normalize,\n onError,\n })\n if (!initResult.ok) {\n return { source: 'initial', parseError: true }\n }\n const pmDoc = yXmlFragmentToProseMirrorRootNode(sharedProseMirror, schema)\n const canonicalText = serialize(pmDoc)\n replaceSharedText(sharedText, canonicalText, ORIGIN_INIT, normalize)\n lastBridgedText = normalize(canonicalText)\n return { source: 'initial' }\n }\n return { source: 'empty' }\n }\n\n if (hasText && !hasProsemirror) {\n const ok = syncTextToProsemirror(ORIGIN_INIT)\n return { source: 'text', ...(!ok && { parseError: true }) }\n }\n\n if (!hasText && hasProsemirror) {\n const textFromProsemirror = sharedProseMirrorToText(sharedProseMirror)\n if (textFromProsemirror !== null) {\n replaceSharedText(sharedText, textFromProsemirror, ORIGIN_INIT, normalize)\n lastBridgedText = normalize(textFromProsemirror)\n return { source: 'prosemirror' }\n }\n return { source: 'prosemirror', parseError: true }\n }\n\n const prosemirrorText = sharedProseMirrorToText(sharedProseMirror)\n if (prosemirrorText === null) {\n const fallbackText = hasText ? text : (options?.initialText ?? '')\n let parseError = false\n if (fallbackText.length > 0) {\n replaceSharedText(sharedText, fallbackText, ORIGIN_INIT, normalize)\n const fallbackResult = replaceSharedProseMirror(doc, sharedProseMirror, fallbackText, ORIGIN_INIT, {\n schema,\n parse,\n normalize,\n onError,\n })\n if (!fallbackResult.ok) parseError = true\n }\n return { source: 'text', ...(parseError && { parseError: true }) }\n }\n\n if (prosemirrorText !== text) {\n const prefer = options?.prefer ?? 'text'\n if (prefer === 'prosemirror') {\n replaceSharedText(sharedText, prosemirrorText, ORIGIN_INIT, normalize)\n lastBridgedText = normalize(prosemirrorText)\n return { source: 'prosemirror' }\n } else {\n const ok = syncTextToProsemirror(ORIGIN_INIT)\n return { source: 'text', ...(!ok && { parseError: true }) }\n }\n } else {\n lastBridgedText = text\n return { source: 'both-match' }\n }\n }\n\n const textObserver = (\n _: unknown,\n transaction: { origin: unknown },\n ) => {\n if (transaction.origin === ORIGIN_PM_TO_TEXT || transaction.origin === ORIGIN_INIT) {\n return\n }\n\n syncTextToProsemirror(ORIGIN_TEXT_TO_PM)\n }\n\n // Run bootstrap synchronously before installing the observer so that\n // an exception during bootstrap cannot leave a dangling observer.\n const bootstrapResult = bootstrap()\n\n sharedText.observe(textObserver)\n\n return {\n bootstrapResult,\n syncToSharedText(doc: Node): ReplaceTextResult {\n const text = serialize(doc)\n const result = replaceSharedText(sharedText, text, ORIGIN_PM_TO_TEXT, normalize)\n // Always update lastBridgedText unless truly failed (detached).\n // 'unchanged' means Y.Text already has this content — still need to\n // record it so the reverse observer doesn't trigger a redundant sync.\n if (result.ok || result.reason === 'unchanged') {\n lastBridgedText = normalize(text)\n }\n return result\n },\n isYjsSyncChange(tr: Transaction): boolean {\n // Internal meta shape from y-prosemirror's ySyncPlugin (tested against ^1.3.x).\n const meta = tr.getMeta(ySyncPluginKey)\n return (\n typeof meta === 'object' &&\n meta !== null &&\n 'isChangeOrigin' in meta &&\n (meta as Record<string, unknown>).isChangeOrigin === true\n )\n },\n dispose() {\n sharedText.unobserve(textObserver)\n },\n }\n}\n","import type { Node, Schema } from 'prosemirror-model'\nimport type { Transaction } from 'prosemirror-state'\nimport type { Serialize, Parse, Normalize, OnError } from '@pm-cm/core'\nimport type { Doc, Text as YText, XmlFragment as YXmlFragment } from 'yjs'\nimport type { ReplaceTextResult } from './bridge.js'\n\n/** Known warning codes emitted by the yjs bridge and plugins. */\nexport type WarningCode = 'bridge-already-wired' | 'sync-failed' | 'ysync-plugin-missing' | 'cursor-sync-not-installed'\n\n/** Structured warning event for non-fatal warnings. */\nexport type WarningEvent = {\n code: WarningCode\n message: string\n}\n\n/**\n * Warning handler callback for non-fatal warnings.\n *\n * Known codes:\n * - `'bridge-already-wired'` — the same bridge handle is wired to multiple plugin instances.\n * - `'sync-failed'` — `syncToSharedText` failed (e.g. Y.Text detached).\n * - `'ysync-plugin-missing'` — ySyncPlugin state is not available; cursor broadcast skipped.\n * - `'cursor-sync-not-installed'` — cursor sync plugin is not installed on the EditorView.\n */\nexport type OnWarning = (event: WarningEvent) => void\n\n/** Yjs transaction origin: text → ProseMirror direction. */\nexport const ORIGIN_TEXT_TO_PM = 'bridge:text-to-prosemirror'\n\n/** Yjs transaction origin: ProseMirror → text direction. */\nexport const ORIGIN_PM_TO_TEXT = 'bridge:prosemirror-to-text'\n\n/** Yjs transaction origin: bootstrap initialization. */\nexport const ORIGIN_INIT = 'bridge:init'\n\n/** Configuration for {@link createYjsBridge}. */\nexport type YjsBridgeConfig = {\n doc: Doc\n sharedText: YText\n sharedProseMirror: YXmlFragment\n schema: Schema\n serialize: Serialize\n parse: Parse\n normalize?: Normalize\n /** Called on non-fatal errors (e.g. parse failures). Defaults to `console.error`. */\n onError?: OnError\n}\n\n/**\n * Result of the bootstrap phase in {@link createYjsBridge}.\n * Indicates which source was used to initialize the shared types.\n */\nexport type BootstrapResult = {\n source: 'text' | 'prosemirror' | 'both-match' | 'empty' | 'initial'\n /** `true` when format conversion (parse or serialize) failed during bootstrap. The bridge is still usable but the affected shared type may be stale. */\n parseError?: boolean\n}\n\n/** Handle returned by {@link createYjsBridge}. */\nexport type YjsBridgeHandle = {\n /** Result of the synchronous bootstrap phase. */\n readonly bootstrapResult: BootstrapResult\n /** Serialize `doc` and push to `Y.Text` using minimal diff. */\n syncToSharedText(doc: Node): ReplaceTextResult\n /** Returns `true` if the transaction originated from `y-prosemirror` sync. */\n isYjsSyncChange(tr: Transaction): boolean\n /** Remove the Y.Text observer. Call when tearing down. */\n dispose(): void\n}\n","import type { Awareness } from 'y-protocols/awareness'\n\n/**\n * Create a Proxy around a Yjs {@link Awareness} that adapts the cursor sync\n * plugin's awareness field for y-prosemirror's `yCursorPlugin`.\n *\n * y-prosemirror (npm 1.3.7) hardcodes `\"cursor\"` in `createDecorations`,\n * but the cursor sync plugin writes to a separate field (default `\"pmCursor\"`)\n * to avoid conflicts with y-codemirror.next's `\"cursor\"` (Y.Text-based).\n *\n * This proxy:\n * - **`getStates()`**: remaps `cursorField` → `\"cursor\"` so yCursorPlugin\n * finds the PM cursor data under the hardcoded `\"cursor\"` key.\n * - **`getLocalState()`**: returns `cursor: null` so yCursorPlugin's\n * `updateCursorInfo` never tries to broadcast its own cursor.\n * - **`setLocalStateField(\"cursor\", …)`**: suppressed (no-op) so\n * yCursorPlugin cannot overwrite the field managed by the sync plugin.\n */\nexport function createAwarenessProxy(awareness: Awareness, cursorField = 'pmCursor'): Awareness {\n return new Proxy(awareness, {\n get(target, prop, receiver) {\n if (prop === 'getLocalState') {\n return () => {\n const state = target.getLocalState()\n // Hide \"cursor\" so yCursorPlugin's updateCursorInfo sees null\n return state ? { ...state, cursor: null } : state\n }\n }\n if (prop === 'setLocalStateField') {\n return (field: string, value: unknown) => {\n // Block yCursorPlugin's writes to \"cursor\"\n if (field === 'cursor') return\n target.setLocalStateField(field, value)\n }\n }\n if (prop === 'getStates') {\n return () => {\n const states = target.getStates()\n // Remap cursorField → \"cursor\" so yCursorPlugin reads PM cursor data\n const remapped = new Map<number, Record<string, unknown>>()\n states.forEach((state, clientId) => {\n remapped.set(clientId, {\n ...state as Record<string, unknown>,\n cursor: (state as Record<string, unknown>)[cursorField] ?? null,\n })\n })\n return remapped\n }\n }\n const value = Reflect.get(target, prop, receiver) as unknown\n return typeof value === 'function' ? (value as Function).bind(target) : value\n },\n }) as Awareness\n}\n","import type { Node, Schema } from 'prosemirror-model'\nimport type { EditorState, Plugin } from 'prosemirror-state'\nimport type { DecorationAttrs } from 'prosemirror-view'\nimport type { Awareness } from 'y-protocols/awareness'\nimport type { Serialize, SerializeWithMap } from '@pm-cm/core'\nimport { initProseMirrorDoc, yCursorPlugin, ySyncPlugin, yUndoPlugin } from 'y-prosemirror'\nimport type { AbstractType, Text as YText, UndoManager } from 'yjs'\nimport type { XmlFragment as YXmlFragment } from 'yjs'\nimport { createAwarenessProxy } from './awareness-proxy.js'\nimport { createBridgeSyncPlugin } from './bridge-sync-plugin.js'\nimport { createCursorSyncPlugin } from './cursor-sync-plugin.js'\nimport type { YjsBridgeHandle, OnWarning } from './types.js'\n\n/** Yjs ↔ ProseMirror node mapping used by `y-prosemirror`. */\nexport type ProseMirrorMapping = Map<AbstractType<unknown>, Node | Node[]>\n\n/** Options forwarded to `yCursorPlugin` from y-prosemirror. */\nexport type YCursorPluginOpts = {\n awarenessStateFilter?: (currentClientId: number, userClientId: number, user: unknown) => boolean\n cursorBuilder?: (user: unknown, clientId: number) => HTMLElement\n selectionBuilder?: (user: unknown, clientId: number) => DecorationAttrs\n getSelection?: (state: EditorState) => unknown\n}\n\n/** Options forwarded to `yUndoPlugin` from y-prosemirror. */\nexport type YUndoPluginOpts = {\n protectedNodes?: Set<string>\n trackedOrigins?: unknown[]\n undoManager?: UndoManager | null\n}\n\n/** Options for {@link createCollabPlugins}. */\nexport type CollabPluginsOptions = {\n /** Shared ProseMirror document in Yjs. */\n sharedProseMirror: YXmlFragment\n awareness: Awareness\n cursorFieldName?: string\n serialize?: Serialize | SerializeWithMap\n /** Awareness field used for CM/Y.Text cursor payloads. Default `'cursor'`. */\n cmCursorFieldName?: string\n /**\n * Enable PM↔CM cursor sync. Default `false`.\n *\n * When enabled, an {@link createAwarenessProxy | awareness proxy} is applied\n * to suppress y-prosemirror's built-in cursor management.\n */\n cursorSync?: boolean\n /**\n * The shared `Y.Text` instance. When provided, the cursor sync plugin also\n * broadcasts CM-format cursor positions so remote `yCollab` instances render them.\n */\n sharedText?: YText\n /**\n * When provided, a bridge sync plugin is inserted before the cursor sync plugin\n * to ensure Y.Text is synced before cursor positions are computed. This guarantees\n * that serialize-based offsets match Y.Text indices.\n */\n bridge?: YjsBridgeHandle\n /** Extra options forwarded to `yCursorPlugin`. */\n yCursorPluginOpts?: YCursorPluginOpts\n /** Extra options forwarded to `yUndoPlugin`. */\n yUndoPluginOpts?: YUndoPluginOpts\n /** Called for non-fatal warnings. Propagated to child plugins. Default `console.warn`. */\n onWarning?: OnWarning\n}\n\n/**\n * Bundle `ySyncPlugin`, `yCursorPlugin`, `yUndoPlugin` from y-prosemirror,\n * plus an optional PM↔CM cursor sync plugin.\n *\n * @throws If `cursorSync: true` but `serialize` is not provided.\n */\nexport function createCollabPlugins(\n schema: Schema,\n options: CollabPluginsOptions,\n): { plugins: Plugin[]; doc: Node; mapping: ProseMirrorMapping } {\n const cursorFieldName = options.cursorFieldName ?? 'pmCursor'\n const enableCursorSync = options.cursorSync ?? false\n const { sharedProseMirror } = options\n\n if (enableCursorSync && !options.serialize) {\n throw new Error('createCollabPlugins: cursorSync requires serialize to be provided')\n }\n const { doc, mapping: rawMapping } = initProseMirrorDoc(sharedProseMirror, schema)\n const mapping = rawMapping as ProseMirrorMapping\n const pmAwareness = enableCursorSync\n ? createAwarenessProxy(options.awareness, cursorFieldName)\n : options.awareness\n\n const plugins: Plugin[] = [\n ySyncPlugin(sharedProseMirror, { mapping: rawMapping }),\n yCursorPlugin(pmAwareness, options.yCursorPluginOpts ?? {}),\n yUndoPlugin(options.yUndoPluginOpts),\n ]\n\n // Bridge sync plugin must run before cursor sync plugin so that\n // Y.Text is updated before cursor positions are computed.\n if (options.bridge) {\n plugins.push(createBridgeSyncPlugin(options.bridge, { onWarning: options.onWarning }))\n }\n\n if (enableCursorSync && options.serialize) {\n plugins.push(\n createCursorSyncPlugin({\n awareness: options.awareness,\n serialize: options.serialize,\n cursorFieldName,\n cmCursorFieldName: options.cmCursorFieldName,\n sharedText: options.sharedText,\n onWarning: options.onWarning,\n }),\n )\n }\n\n return { plugins, doc, mapping }\n}\n","import { Plugin, PluginKey } from 'prosemirror-state'\nimport type { EditorView } from 'prosemirror-view'\nimport type { YjsBridgeHandle, OnWarning } from './types.js'\n\ntype BridgeSyncState = { needsSync: boolean }\n\ntype BridgeSyncFailure = { ok: false; reason: 'detached' }\n\n/** Options for {@link createBridgeSyncPlugin}. */\nexport type BridgeSyncPluginOptions = {\n /** Called when `syncToSharedText` fails (excludes `reason: 'unchanged'`). */\n onSyncFailure?: (result: BridgeSyncFailure, view: EditorView) => void\n /** Called for non-fatal warnings. Default `console.warn`. */\n onWarning?: OnWarning\n}\n\n/** ProseMirror plugin key for {@link createBridgeSyncPlugin}. Use to read the plugin state. */\nexport const bridgeSyncPluginKey = new PluginKey<BridgeSyncState>('pm-cm-bridge-sync')\n\nconst wiredBridges = new WeakSet<YjsBridgeHandle>()\n\nconst defaultOnWarning: OnWarning = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`)\n\n/**\n * ProseMirror plugin that automatically syncs PM doc changes to Y.Text\n * via the bridge handle. Skips Yjs-originated changes to avoid loops.\n *\n * A warning is logged if the same bridge handle is wired more than once.\n * The guard is cleaned up when the plugin is destroyed.\n */\nexport function createBridgeSyncPlugin(\n bridge: YjsBridgeHandle,\n options: BridgeSyncPluginOptions = {},\n): Plugin {\n const warn = options.onWarning ?? defaultOnWarning\n if (wiredBridges.has(bridge)) {\n warn({ code: 'bridge-already-wired', message: 'this bridge is already wired to another plugin instance' })\n }\n wiredBridges.add(bridge)\n\n return new Plugin<BridgeSyncState>({\n key: bridgeSyncPluginKey,\n\n state: {\n init(): BridgeSyncState {\n return { needsSync: false }\n },\n apply(tr, _prev): BridgeSyncState {\n if (!tr.docChanged) return { needsSync: false }\n if (bridge.isYjsSyncChange(tr)) return { needsSync: false }\n return { needsSync: true }\n },\n },\n\n view() {\n return {\n update(view) {\n const state = bridgeSyncPluginKey.getState(view.state)\n if (state?.needsSync) {\n const result = bridge.syncToSharedText(view.state.doc)\n if (!result.ok) {\n if (result.reason === 'detached') {\n options.onSyncFailure?.(result, view)\n warn({ code: 'sync-failed', message: `bridge sync failed: ${result.reason}` })\n }\n }\n }\n },\n destroy() {\n wiredBridges.delete(bridge)\n },\n }\n },\n })\n}\n","import { Plugin, PluginKey } from 'prosemirror-state'\nimport type { Node } from 'prosemirror-model'\nimport type { EditorView } from 'prosemirror-view'\nimport type { Awareness } from 'y-protocols/awareness'\nimport { absolutePositionToRelativePosition, ySyncPluginKey } from 'y-prosemirror'\nimport { createRelativePositionFromTypeIndex } from 'yjs'\nimport type { Text as YText, XmlFragment as YXmlFragment } from 'yjs'\nimport type { Serialize, SerializeWithMap, CursorMap } from '@pm-cm/core'\nimport { buildCursorMap, cursorMapLookup, reverseCursorMapLookup } from '@pm-cm/core'\nimport type { OnWarning } from './types.js'\n\n/** Plugin state for the cursor sync plugin. Read via {@link cursorSyncPluginKey}. */\nexport type CursorSyncState = {\n /** Pending CodeMirror cursor to broadcast. Set by {@link syncCmCursor}. */\n pendingCm: { anchor: number; head: number } | null\n /** Text offset mapped from the current PM selection anchor. `null` when no mapping is available. */\n mappedTextOffset: number | null\n}\n\n/** ProseMirror plugin key for {@link createCursorSyncPlugin}. Use to read the plugin state. */\nexport const cursorSyncPluginKey = new PluginKey<CursorSyncState>('pm-cm-cursor-sync')\n\n/**\n * Internal shape of `ySyncPluginKey` state from y-prosemirror.\n * Not exported by upstream — kept here for explicit tracking.\n * Tested against y-prosemirror ^1.3.x.\n */\ntype YSyncPluginState = { type: YXmlFragment; binding: { mapping: Map<unknown, unknown> } }\n\nfunction getYSyncState(view: EditorView): YSyncPluginState | null {\n const raw = ySyncPluginKey.getState(view.state) as Record<string, unknown> | undefined\n if (!raw) return null\n if (\n typeof raw === 'object' &&\n 'type' in raw && raw.type &&\n 'binding' in raw && raw.binding &&\n typeof raw.binding === 'object' &&\n 'mapping' in (raw.binding as Record<string, unknown>) &&\n (raw.binding as Record<string, unknown>).mapping instanceof Map\n ) {\n return raw as unknown as YSyncPluginState\n }\n return null\n}\n\nfunction toRelativePosition(\n view: EditorView,\n pmPos: number,\n): unknown | null {\n const ySyncState = getYSyncState(view)\n if (!ySyncState) return null\n\n return absolutePositionToRelativePosition(\n pmPos,\n ySyncState.type,\n ySyncState.binding.mapping as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- y-prosemirror internal mapping type\n )\n}\n\n/** Returns `false` when ySyncPlugin state is unavailable (plugin not installed). */\nfunction broadcastPmCursor(\n awareness: Awareness,\n cursorFieldName: string,\n view: EditorView,\n pmAnchor: number,\n pmHead: number,\n): boolean {\n const relAnchor = toRelativePosition(view, pmAnchor)\n const relHead = toRelativePosition(view, pmHead)\n if (relAnchor === null || relHead === null) return false\n\n awareness.setLocalStateField(cursorFieldName, { anchor: relAnchor, head: relHead })\n return true\n}\n\nfunction broadcastTextCursor(\n awareness: Awareness,\n cmCursorFieldName: string,\n sharedText: YText,\n textAnchor: number,\n textHead: number,\n): void {\n const len = sharedText.length\n const clamp = (v: number) => Math.max(0, Math.min(v, len))\n const relAnchor = createRelativePositionFromTypeIndex(sharedText, clamp(textAnchor))\n const relHead = createRelativePositionFromTypeIndex(sharedText, clamp(textHead))\n awareness.setLocalStateField(cmCursorFieldName, { anchor: relAnchor, head: relHead })\n}\n\nconst defaultOnWarning: OnWarning = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`)\n\n/** Options for {@link createCursorSyncPlugin}. */\nexport type CursorSyncPluginOptions = {\n awareness: Awareness\n serialize: Serialize | SerializeWithMap\n cursorFieldName?: string\n /** Awareness field used for CM/Y.Text cursor payloads. Default `'cursor'`. */\n cmCursorFieldName?: string\n /**\n * When provided, the plugin also broadcasts CM-format cursor positions\n * (Y.Text relative positions) to the awareness field specified by\n * `cmCursorFieldName`, so that remote `yCollab` instances can render the cursor.\n */\n sharedText?: YText\n /** Called for non-fatal warnings. Default `console.warn`. */\n onWarning?: OnWarning\n}\n\n/**\n * ProseMirror plugin that synchronizes cursor positions between PM and CM via Yjs awareness.\n *\n * - PM → awareness: automatically broadcasts when the PM view is focused and selection changes.\n * - CM → awareness: triggered by dispatching {@link syncCmCursor}.\n */\nexport function createCursorSyncPlugin(options: CursorSyncPluginOptions): Plugin {\n const { awareness, serialize, sharedText } = options\n const warn = options.onWarning ?? defaultOnWarning\n const cursorFieldName = options.cursorFieldName ?? 'pmCursor'\n const cmCursorFieldName = options.cmCursorFieldName ?? 'cursor'\n\n let warnedSyncPluginMissing = false\n\n // Cached cursor map (serialize-based) — rebuilt only when doc changes\n let cachedMap: CursorMap | null = null\n let cachedMapDoc: Node | null = null\n\n function getOrBuildMap(doc: Node): CursorMap {\n if (cachedMapDoc !== doc || !cachedMap) {\n cachedMap = buildCursorMap(doc, serialize)\n cachedMapDoc = doc\n }\n return cachedMap\n }\n\n return new Plugin<CursorSyncState>({\n key: cursorSyncPluginKey,\n\n state: {\n init(): CursorSyncState {\n return { pendingCm: null, mappedTextOffset: null }\n },\n apply(tr, prev, _oldState, newState): CursorSyncState {\n const cmMeta = tr.getMeta(cursorSyncPluginKey) as\n | { anchor: number; head: number }\n | undefined\n if (cmMeta) {\n return { pendingCm: cmMeta, mappedTextOffset: prev.mappedTextOffset }\n }\n\n // Compute PM → text offset when selection or doc changes\n let mappedTextOffset = prev.mappedTextOffset\n if (tr.selectionSet || tr.docChanged) {\n const map = getOrBuildMap(newState.doc)\n mappedTextOffset = cursorMapLookup(map, newState.selection.anchor)\n }\n\n return {\n pendingCm: prev.pendingCm !== null ? null : prev.pendingCm,\n mappedTextOffset,\n }\n },\n },\n\n view() {\n return {\n update(view, prevState) {\n const pluginState = cursorSyncPluginKey.getState(view.state)\n const prevPluginState = cursorSyncPluginKey.getState(prevState)\n\n // CM → awareness: broadcast when pendingCm is newly set\n if (\n pluginState?.pendingCm != null &&\n pluginState.pendingCm !== prevPluginState?.pendingCm\n ) {\n const map = getOrBuildMap(view.state.doc)\n const pmAnchor = reverseCursorMapLookup(map, pluginState.pendingCm.anchor)\n const pmHead = reverseCursorMapLookup(map, pluginState.pendingCm.head)\n if (pmAnchor !== null && pmHead !== null) {\n const ok = broadcastPmCursor(awareness, cursorFieldName, view, pmAnchor, pmHead)\n if (!ok && !warnedSyncPluginMissing) {\n warnedSyncPluginMissing = true\n warn({ code: 'ysync-plugin-missing', message: 'ySyncPlugin state not available — cursor broadcast skipped' })\n }\n }\n // Also broadcast CM-format cursor so remote yCollab can render it\n if (sharedText) {\n broadcastTextCursor(\n awareness,\n cmCursorFieldName,\n sharedText,\n pluginState.pendingCm.anchor,\n pluginState.pendingCm.head,\n )\n }\n return\n }\n\n // PM → awareness: auto-broadcast on selection/doc change when focused\n if (\n view.hasFocus() &&\n (view.state.selection !== prevState.selection ||\n view.state.doc !== prevState.doc)\n ) {\n const { anchor, head } = view.state.selection\n const ok = broadcastPmCursor(awareness, cursorFieldName, view, anchor, head)\n if (!ok && !warnedSyncPluginMissing) {\n warnedSyncPluginMissing = true\n warn({ code: 'ysync-plugin-missing', message: 'ySyncPlugin state not available — cursor broadcast skipped' })\n }\n // Also broadcast CM-format cursor so remote yCollab can render it.\n // When bridgeSyncPlugin runs before this plugin, Y.Text is already\n // synced so serialize-based offsets match Y.Text indices.\n if (sharedText) {\n const map = getOrBuildMap(view.state.doc)\n const textAnchor = cursorMapLookup(map, anchor)\n const textHead = cursorMapLookup(map, head)\n if (textAnchor !== null && textHead !== null) {\n broadcastTextCursor(awareness, cmCursorFieldName, sharedText, textAnchor, textHead)\n }\n }\n }\n },\n }\n },\n })\n}\n\n/**\n * Dispatch a CodeMirror cursor offset (or range) to the cursor sync plugin.\n * The plugin will convert it to a ProseMirror position and broadcast via awareness.\n *\n * @param view - The ProseMirror EditorView that has the cursor sync plugin installed.\n * @param anchor - CodeMirror text offset for the anchor.\n * @param head - CodeMirror text offset for the head (defaults to `anchor` for a collapsed cursor).\n * @param onWarning - Optional warning callback. Default `console.warn`.\n */\nexport function syncCmCursor(view: EditorView, anchor: number, head?: number, onWarning?: OnWarning): void {\n if (!cursorSyncPluginKey.getState(view.state)) {\n (onWarning ?? defaultOnWarning)({ code: 'cursor-sync-not-installed', message: 'cursor sync plugin is not installed on this EditorView' })\n return\n }\n const sanitize = (v: number) => Math.max(0, Math.floor(v))\n view.dispatch(\n view.state.tr.setMeta(cursorSyncPluginKey, {\n anchor: sanitize(anchor),\n head: sanitize(head ?? anchor),\n }),\n )\n}\n"],"mappings":"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","names":["doc","value","import_y_prosemirror","import_prosemirror_state","import_y_prosemirror","defaultOnWarning","import_core"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/bridge.ts","../src/types.ts","../src/awareness-proxy.ts","../src/collab-plugins.ts","../src/bridge-sync-plugin.ts","../src/cursor-sync-plugin.ts"],"sourcesContent":["export { createYjsBridge, replaceSharedText, replaceSharedProseMirror } from './bridge.js'\nexport type { YjsBridgeOptions, ReplaceResult, ReplaceTextResult, ReplaceProseMirrorResult } from './bridge.js'\nexport { createAwarenessProxy } from './awareness-proxy.js'\nexport { createCollabPlugins } from './collab-plugins.js'\nexport type { CollabPluginsOptions, ProseMirrorMapping, YCursorPluginOpts, YUndoPluginOpts } from './collab-plugins.js'\nexport {\n ORIGIN_TEXT_TO_PM,\n ORIGIN_PM_TO_TEXT,\n ORIGIN_INIT,\n} from './types.js'\nexport type {\n BootstrapResult,\n YjsBridgeConfig,\n YjsBridgeHandle,\n WarningCode,\n WarningEvent,\n OnWarning,\n} from './types.js'\n\n// Cursor mapping re-exported from @pm-cm/core\nexport { buildCursorMap, cursorMapLookup, reverseCursorMapLookup } from '@pm-cm/core'\nexport type { TextSegment, CursorMap, SerializeWithMap, CursorMapWriter, Matcher, MatchResult, MatchRun } from '@pm-cm/core'\n\n// Bridge sync plugin (auto PM→Y.Text wiring)\nexport { createBridgeSyncPlugin, bridgeSyncPluginKey } from './bridge-sync-plugin.js'\nexport type { BridgeSyncPluginOptions } from './bridge-sync-plugin.js'\n\n// Cursor sync plugin\nexport { createCursorSyncPlugin, cursorSyncPluginKey, syncCmCursor } from './cursor-sync-plugin.js'\nexport type { CursorSyncState, CursorSyncPluginOptions } from './cursor-sync-plugin.js'\n\n// Re-export types from @pm-cm/core\nexport { createCursorMapWriter, wrapSerialize } from '@pm-cm/core'\nexport type { Serialize, Parse, Normalize, OnError, ErrorCode, ErrorEvent } from '@pm-cm/core'\n","import type { Node } from 'prosemirror-model'\nimport type { Transaction } from 'prosemirror-state'\nimport { prosemirrorToYXmlFragment, yXmlFragmentToProseMirrorRootNode, ySyncPluginKey } from 'y-prosemirror'\nimport type { Doc, Text as YText, XmlFragment as YXmlFragment } from 'yjs'\nimport type { Normalize, OnError } from '@pm-cm/core'\nimport type { BootstrapResult, YjsBridgeConfig, YjsBridgeHandle } from './types.js'\nimport { ORIGIN_INIT, ORIGIN_TEXT_TO_PM, ORIGIN_PM_TO_TEXT } from './types.js'\n\nconst defaultNormalize: Normalize = (s) => s.replace(/\\r\\n?/g, '\\n')\nconst defaultOnError: OnError = (event) => console.error(`[bridge] ${event.code}: ${event.message}`, event.cause)\n\n/** Result of {@link replaceSharedText}. */\nexport type ReplaceTextResult =\n | { ok: true }\n | { ok: false; reason: 'unchanged' }\n | { ok: false; reason: 'detached' }\n | { ok: false; reason: 'serialize-error' }\n\n/** Result of {@link replaceSharedProseMirror}. */\nexport type ReplaceProseMirrorResult =\n | { ok: true }\n | { ok: false; reason: 'parse-error' }\n | { ok: false; reason: 'detached' }\n\n/**\n * Union of all replace-result types. Kept for backward compatibility.\n * Prefer the narrower {@link ReplaceTextResult} / {@link ReplaceProseMirrorResult}.\n */\nexport type ReplaceResult = ReplaceTextResult | ReplaceProseMirrorResult\n\n/**\n * Replace `Y.Text` content using a minimal diff (common prefix/suffix trimming).\n * Returns a {@link ReplaceResult} indicating success or failure reason.\n */\nexport function replaceSharedText(\n sharedText: YText,\n next: string,\n origin: unknown,\n normalize: Normalize = defaultNormalize,\n): ReplaceTextResult {\n if (!sharedText.doc) {\n return { ok: false, reason: 'detached' }\n }\n\n const normalized = normalize(next)\n const current = sharedText.toString()\n if (current === normalized) {\n return { ok: false, reason: 'unchanged' }\n }\n\n // Minimal diff: find common prefix and suffix, replace only the changed middle.\n let start = 0\n const minLen = Math.min(current.length, normalized.length)\n while (start < minLen && current.charCodeAt(start) === normalized.charCodeAt(start)) {\n start++\n }\n\n let endCurrent = current.length\n let endNext = normalized.length\n while (endCurrent > start && endNext > start && current.charCodeAt(endCurrent - 1) === normalized.charCodeAt(endNext - 1)) {\n endCurrent--\n endNext--\n }\n\n sharedText.doc.transact(() => {\n const deleteCount = endCurrent - start\n if (deleteCount > 0) {\n sharedText.delete(start, deleteCount)\n }\n const insertStr = normalized.slice(start, endNext)\n if (insertStr.length > 0) {\n sharedText.insert(start, insertStr)\n }\n }, origin)\n\n return { ok: true }\n}\n\n/**\n * Replace `Y.XmlFragment` by parsing serialized text into a ProseMirror document.\n * Returns a {@link ReplaceResult} indicating success or failure reason.\n */\nexport function replaceSharedProseMirror(\n doc: Doc,\n fragment: YXmlFragment,\n text: string,\n origin: unknown,\n config: Pick<YjsBridgeConfig, 'schema' | 'parse' | 'normalize' | 'onError'>,\n): ReplaceProseMirrorResult {\n if (!fragment.doc) {\n return { ok: false, reason: 'detached' }\n }\n if (fragment.doc !== doc) {\n throw new Error('fragment belongs to a different Y.Doc than the provided doc')\n }\n\n const normalize = config.normalize ?? defaultNormalize\n const onError = config.onError ?? defaultOnError\n let nextDoc: Node\n try {\n nextDoc = config.parse(normalize(text), config.schema)\n } catch (error) {\n onError({ code: 'parse-error', message: 'failed to parse text into ProseMirror document', cause: error })\n return { ok: false, reason: 'parse-error' }\n }\n\n doc.transact(() => {\n prosemirrorToYXmlFragment(nextDoc, fragment)\n }, origin)\n return { ok: true }\n}\n\n/** Options for {@link createYjsBridge}. */\nexport type YjsBridgeOptions = {\n initialText?: string\n /** Which side wins when both sharedText and sharedProseMirror exist and differ. Default `'text'`. */\n prefer?: 'text' | 'prosemirror'\n}\n\n/**\n * Create a collaborative bridge that keeps `Y.Text` and `Y.XmlFragment` in sync.\n *\n * Runs a synchronous bootstrap to reconcile existing state, then installs a\n * `Y.Text` observer for the text → ProseMirror direction.\n *\n * @throws If `sharedText` or `sharedProseMirror` belong to a different `Y.Doc`.\n */\nexport function createYjsBridge(\n config: YjsBridgeConfig,\n options?: YjsBridgeOptions,\n): YjsBridgeHandle {\n const {\n doc,\n sharedText,\n sharedProseMirror,\n schema,\n serialize,\n parse,\n } = config\n const normalize = config.normalize ?? defaultNormalize\n const onError = config.onError ?? defaultOnError\n\n if (!sharedText.doc) {\n throw new Error('sharedText is not attached to any Y.Doc')\n }\n if (sharedText.doc !== doc) {\n throw new Error('sharedText belongs to a different Y.Doc than the provided doc')\n }\n if (!sharedProseMirror.doc) {\n throw new Error('sharedProseMirror is not attached to any Y.Doc')\n }\n if (sharedProseMirror.doc !== doc) {\n throw new Error('sharedProseMirror belongs to a different Y.Doc than the provided doc')\n }\n\n let lastBridgedText: string | null = null\n\n /** Returns `true` if the parse succeeded. */\n const syncTextToProsemirror = (origin: unknown): boolean => {\n const text = normalize(sharedText.toString())\n if (lastBridgedText === text) {\n return true\n }\n\n const result = replaceSharedProseMirror(doc, sharedProseMirror, text, origin, {\n schema,\n parse,\n normalize,\n onError,\n })\n if (result.ok) {\n lastBridgedText = text\n }\n return result.ok\n }\n\n const sharedProseMirrorToText = (fragment: YXmlFragment): string | null => {\n try {\n const pmDoc = yXmlFragmentToProseMirrorRootNode(fragment, schema)\n return normalize(serialize(pmDoc))\n } catch (error) {\n onError({ code: 'serialize-error', message: 'failed to convert ProseMirror fragment to text', cause: error })\n return null\n }\n }\n\n // Bootstrap\n const bootstrap = (): BootstrapResult => {\n const text = normalize(sharedText.toString())\n const hasText = text.length > 0\n const hasProsemirror = sharedProseMirror.length > 0\n\n if (!hasText && !hasProsemirror) {\n const initial = options?.initialText ?? ''\n if (initial.length > 0) {\n // Set Y.XmlFragment first, then derive Y.Text from serialize(parse(initial))\n // to ensure both shared types are in the same canonical form.\n const initResult = replaceSharedProseMirror(doc, sharedProseMirror, initial, ORIGIN_INIT, {\n schema,\n parse,\n normalize,\n onError,\n })\n if (!initResult.ok) {\n return { source: 'initial', parseError: true }\n }\n const pmDoc = yXmlFragmentToProseMirrorRootNode(sharedProseMirror, schema)\n let canonicalText: string\n try {\n canonicalText = serialize(pmDoc)\n } catch (error) {\n onError({ code: 'serialize-error', message: 'failed to serialize ProseMirror document during bootstrap', cause: error })\n return { source: 'initial', parseError: true }\n }\n replaceSharedText(sharedText, canonicalText, ORIGIN_INIT, normalize)\n lastBridgedText = normalize(canonicalText)\n return { source: 'initial' }\n }\n return { source: 'empty' }\n }\n\n if (hasText && !hasProsemirror) {\n const ok = syncTextToProsemirror(ORIGIN_INIT)\n return { source: 'text', ...(!ok && { parseError: true }) }\n }\n\n if (!hasText && hasProsemirror) {\n const textFromProsemirror = sharedProseMirrorToText(sharedProseMirror)\n if (textFromProsemirror !== null) {\n replaceSharedText(sharedText, textFromProsemirror, ORIGIN_INIT, normalize)\n lastBridgedText = normalize(textFromProsemirror)\n return { source: 'prosemirror' }\n }\n return { source: 'prosemirror', parseError: true }\n }\n\n const prosemirrorText = sharedProseMirrorToText(sharedProseMirror)\n if (prosemirrorText === null) {\n const fallbackText = hasText ? text : (options?.initialText ?? '')\n let parseError = false\n if (fallbackText.length > 0) {\n replaceSharedText(sharedText, fallbackText, ORIGIN_INIT, normalize)\n const fallbackResult = replaceSharedProseMirror(doc, sharedProseMirror, fallbackText, ORIGIN_INIT, {\n schema,\n parse,\n normalize,\n onError,\n })\n if (!fallbackResult.ok) parseError = true\n }\n return { source: 'text', ...(parseError && { parseError: true }) }\n }\n\n if (prosemirrorText !== text) {\n const prefer = options?.prefer ?? 'text'\n if (prefer === 'prosemirror') {\n replaceSharedText(sharedText, prosemirrorText, ORIGIN_INIT, normalize)\n lastBridgedText = normalize(prosemirrorText)\n return { source: 'prosemirror' }\n } else {\n const ok = syncTextToProsemirror(ORIGIN_INIT)\n return { source: 'text', ...(!ok && { parseError: true }) }\n }\n } else {\n lastBridgedText = text\n return { source: 'both-match' }\n }\n }\n\n const textObserver = (\n _: unknown,\n transaction: { origin: unknown },\n ) => {\n if (transaction.origin === ORIGIN_PM_TO_TEXT || transaction.origin === ORIGIN_INIT) {\n return\n }\n\n syncTextToProsemirror(ORIGIN_TEXT_TO_PM)\n }\n\n // Run bootstrap synchronously before installing the observer so that\n // an exception during bootstrap cannot leave a dangling observer.\n const bootstrapResult = bootstrap()\n\n sharedText.observe(textObserver)\n\n return {\n bootstrapResult,\n syncToSharedText(doc: Node): ReplaceTextResult {\n let text: string\n try {\n text = serialize(doc)\n } catch (error) {\n onError({ code: 'serialize-error', message: 'failed to serialize ProseMirror document', cause: error })\n return { ok: false, reason: 'serialize-error' }\n }\n const result = replaceSharedText(sharedText, text, ORIGIN_PM_TO_TEXT, normalize)\n // Always update lastBridgedText unless truly failed (detached).\n // 'unchanged' means Y.Text already has this content — still need to\n // record it so the reverse observer doesn't trigger a redundant sync.\n if (result.ok || result.reason === 'unchanged') {\n lastBridgedText = normalize(text)\n }\n return result\n },\n isYjsSyncChange(tr: Transaction): boolean {\n // Internal meta shape from y-prosemirror's ySyncPlugin (tested against ^1.3.x).\n const meta = tr.getMeta(ySyncPluginKey)\n return (\n typeof meta === 'object' &&\n meta !== null &&\n 'isChangeOrigin' in meta &&\n (meta as Record<string, unknown>).isChangeOrigin === true\n )\n },\n dispose() {\n sharedText.unobserve(textObserver)\n },\n }\n}\n","import type { Node, Schema } from 'prosemirror-model'\nimport type { Transaction } from 'prosemirror-state'\nimport type { Serialize, Parse, Normalize, OnError } from '@pm-cm/core'\nimport type { Doc, Text as YText, XmlFragment as YXmlFragment } from 'yjs'\nimport type { ReplaceTextResult } from './bridge.js'\n\n/** Known warning codes emitted by the yjs bridge and plugins. */\nexport type WarningCode = 'bridge-already-wired' | 'sync-failed' | 'ysync-plugin-missing' | 'cursor-sync-not-installed' | 'cursor-map-error'\n\n/** Structured warning event for non-fatal warnings. */\nexport type WarningEvent = {\n code: WarningCode\n message: string\n}\n\n/**\n * Warning handler callback for non-fatal warnings.\n *\n * Known codes:\n * - `'bridge-already-wired'` — the same bridge handle is wired to multiple plugin instances.\n * - `'sync-failed'` — `syncToSharedText` failed (e.g. Y.Text detached).\n * - `'ysync-plugin-missing'` — ySyncPlugin state is not available; cursor broadcast skipped.\n * - `'cursor-sync-not-installed'` — cursor sync plugin is not installed on the EditorView.\n * - `'cursor-map-error'` — failed to build cursor map (serialize threw); cursor sync skipped.\n */\nexport type OnWarning = (event: WarningEvent) => void\n\n/** Yjs transaction origin: text → ProseMirror direction. */\nexport const ORIGIN_TEXT_TO_PM = 'bridge:text-to-prosemirror'\n\n/** Yjs transaction origin: ProseMirror → text direction. */\nexport const ORIGIN_PM_TO_TEXT = 'bridge:prosemirror-to-text'\n\n/** Yjs transaction origin: bootstrap initialization. */\nexport const ORIGIN_INIT = 'bridge:init'\n\n/** Configuration for {@link createYjsBridge}. */\nexport type YjsBridgeConfig = {\n doc: Doc\n sharedText: YText\n sharedProseMirror: YXmlFragment\n schema: Schema\n serialize: Serialize\n parse: Parse\n normalize?: Normalize\n /** Called on non-fatal errors (e.g. parse failures). Defaults to `console.error`. */\n onError?: OnError\n}\n\n/**\n * Result of the bootstrap phase in {@link createYjsBridge}.\n * Indicates which source was used to initialize the shared types.\n */\nexport type BootstrapResult = {\n source: 'text' | 'prosemirror' | 'both-match' | 'empty' | 'initial'\n /** `true` when format conversion (parse or serialize) failed during bootstrap. The bridge is still usable but the affected shared type may be stale. */\n parseError?: boolean\n}\n\n/** Handle returned by {@link createYjsBridge}. */\nexport type YjsBridgeHandle = {\n /** Result of the synchronous bootstrap phase. */\n readonly bootstrapResult: BootstrapResult\n /** Serialize `doc` and push to `Y.Text` using minimal diff. */\n syncToSharedText(doc: Node): ReplaceTextResult\n /** Returns `true` if the transaction originated from `y-prosemirror` sync. */\n isYjsSyncChange(tr: Transaction): boolean\n /** Remove the Y.Text observer. Call when tearing down. */\n dispose(): void\n}\n","import type { Awareness } from 'y-protocols/awareness'\n\n/**\n * Create a Proxy around a Yjs {@link Awareness} that adapts the cursor sync\n * plugin's awareness field for y-prosemirror's `yCursorPlugin`.\n *\n * y-prosemirror (npm 1.3.7) hardcodes `\"cursor\"` in `createDecorations`,\n * but the cursor sync plugin writes to a separate field (default `\"pmCursor\"`)\n * to avoid conflicts with y-codemirror.next's `\"cursor\"` (Y.Text-based).\n *\n * This proxy:\n * - **`getStates()`**: remaps `cursorField` → `\"cursor\"` so yCursorPlugin\n * finds the PM cursor data under the hardcoded `\"cursor\"` key.\n * - **`getLocalState()`**: returns `cursor: null` so yCursorPlugin's\n * `updateCursorInfo` never tries to broadcast its own cursor.\n * - **`setLocalStateField(\"cursor\", …)`**: suppressed (no-op) so\n * yCursorPlugin cannot overwrite the field managed by the sync plugin.\n */\nexport function createAwarenessProxy(awareness: Awareness, cursorField = 'pmCursor'): Awareness {\n return new Proxy(awareness, {\n get(target, prop, receiver) {\n if (prop === 'getLocalState') {\n return () => {\n const state = target.getLocalState()\n // Hide \"cursor\" so yCursorPlugin's updateCursorInfo sees null\n return state ? { ...state, cursor: null } : state\n }\n }\n if (prop === 'setLocalStateField') {\n return (field: string, value: unknown) => {\n // Block yCursorPlugin's writes to \"cursor\"\n if (field === 'cursor') return\n target.setLocalStateField(field, value)\n }\n }\n if (prop === 'getStates') {\n return () => {\n const states = target.getStates()\n // Remap cursorField → \"cursor\" so yCursorPlugin reads PM cursor data.\n // Only override when the client actually has cursorField — clients\n // without cursor sync (e.g. using yCursorPlugin directly) keep their\n // original \"cursor\" intact.\n const remapped = new Map<number, Record<string, unknown>>()\n states.forEach((state, clientId) => {\n const s = state as Record<string, unknown>\n if (cursorField in s) {\n remapped.set(clientId, { ...s, cursor: s[cursorField] ?? null })\n } else {\n remapped.set(clientId, s)\n }\n })\n return remapped\n }\n }\n const value = Reflect.get(target, prop, receiver) as unknown\n return typeof value === 'function' ? (value as Function).bind(target) : value\n },\n }) as Awareness\n}\n","import type { Node, Schema } from 'prosemirror-model'\nimport type { EditorState, Plugin } from 'prosemirror-state'\nimport type { DecorationAttrs } from 'prosemirror-view'\nimport type { Awareness } from 'y-protocols/awareness'\nimport type { Serialize, SerializeWithMap } from '@pm-cm/core'\nimport { initProseMirrorDoc, yCursorPlugin, ySyncPlugin, yUndoPlugin } from 'y-prosemirror'\nimport type { AbstractType, Text as YText, UndoManager } from 'yjs'\nimport type { XmlFragment as YXmlFragment } from 'yjs'\nimport { createAwarenessProxy } from './awareness-proxy.js'\nimport { createBridgeSyncPlugin } from './bridge-sync-plugin.js'\nimport { createCursorSyncPlugin } from './cursor-sync-plugin.js'\nimport type { YjsBridgeHandle, OnWarning } from './types.js'\n\n/** Yjs ↔ ProseMirror node mapping used by `y-prosemirror`. */\nexport type ProseMirrorMapping = Map<AbstractType<unknown>, Node | Node[]>\n\n/** Options forwarded to `yCursorPlugin` from y-prosemirror. */\nexport type YCursorPluginOpts = {\n awarenessStateFilter?: (currentClientId: number, userClientId: number, user: unknown) => boolean\n cursorBuilder?: (user: unknown, clientId: number) => HTMLElement\n selectionBuilder?: (user: unknown, clientId: number) => DecorationAttrs\n getSelection?: (state: EditorState) => unknown\n}\n\n/** Options forwarded to `yUndoPlugin` from y-prosemirror. */\nexport type YUndoPluginOpts = {\n protectedNodes?: Set<string>\n trackedOrigins?: unknown[]\n undoManager?: UndoManager | null\n}\n\n/** Options for {@link createCollabPlugins}. */\nexport type CollabPluginsOptions = {\n /** Shared ProseMirror document in Yjs. */\n sharedProseMirror: YXmlFragment\n awareness: Awareness\n cursorFieldName?: string\n serialize?: Serialize | SerializeWithMap\n /** Awareness field used for CM/Y.Text cursor payloads. Default `'cursor'`. */\n cmCursorFieldName?: string\n /**\n * Enable PM↔CM cursor sync. Default `false`.\n *\n * When enabled, an {@link createAwarenessProxy | awareness proxy} is applied\n * to suppress y-prosemirror's built-in cursor management.\n */\n cursorSync?: boolean\n /**\n * The shared `Y.Text` instance. When provided, the cursor sync plugin also\n * broadcasts CM-format cursor positions so remote `yCollab` instances render them.\n */\n sharedText?: YText\n /**\n * When provided, a bridge sync plugin is inserted before the cursor sync plugin\n * to ensure Y.Text is synced before cursor positions are computed. This guarantees\n * that serialize-based offsets match Y.Text indices.\n */\n bridge?: YjsBridgeHandle\n /** Extra options forwarded to `yCursorPlugin`. */\n yCursorPluginOpts?: YCursorPluginOpts\n /** Extra options forwarded to `yUndoPlugin`. */\n yUndoPluginOpts?: YUndoPluginOpts\n /** Called for non-fatal warnings. Propagated to child plugins. Default `console.warn`. */\n onWarning?: OnWarning\n}\n\n/**\n * Bundle `ySyncPlugin`, `yCursorPlugin`, `yUndoPlugin` from y-prosemirror,\n * plus an optional PM↔CM cursor sync plugin.\n *\n * @throws If `cursorSync: true` but `serialize` is not provided.\n */\nexport function createCollabPlugins(\n schema: Schema,\n options: CollabPluginsOptions,\n): { plugins: Plugin[]; doc: Node; mapping: ProseMirrorMapping } {\n const cursorFieldName = options.cursorFieldName ?? 'pmCursor'\n const enableCursorSync = options.cursorSync ?? false\n const { sharedProseMirror } = options\n\n if (enableCursorSync && !options.serialize) {\n throw new Error('createCollabPlugins: cursorSync requires serialize to be provided')\n }\n const { doc, mapping: rawMapping } = initProseMirrorDoc(sharedProseMirror, schema)\n const mapping = rawMapping as ProseMirrorMapping\n const pmAwareness = enableCursorSync\n ? createAwarenessProxy(options.awareness, cursorFieldName)\n : options.awareness\n\n const plugins: Plugin[] = [\n ySyncPlugin(sharedProseMirror, { mapping: rawMapping }),\n yCursorPlugin(pmAwareness, options.yCursorPluginOpts ?? {}),\n yUndoPlugin(options.yUndoPluginOpts),\n ]\n\n // Bridge sync plugin must run before cursor sync plugin so that\n // Y.Text is updated before cursor positions are computed.\n if (options.bridge) {\n plugins.push(createBridgeSyncPlugin(options.bridge, { onWarning: options.onWarning }))\n }\n\n if (enableCursorSync && options.serialize) {\n plugins.push(\n createCursorSyncPlugin({\n awareness: options.awareness,\n serialize: options.serialize,\n cursorFieldName,\n cmCursorFieldName: options.cmCursorFieldName,\n sharedText: options.sharedText,\n onWarning: options.onWarning,\n }),\n )\n }\n\n return { plugins, doc, mapping }\n}\n","import { Plugin, PluginKey } from 'prosemirror-state'\nimport type { EditorView } from 'prosemirror-view'\nimport type { YjsBridgeHandle, OnWarning } from './types.js'\n\ntype BridgeSyncState = { needsSync: boolean }\n\ntype BridgeSyncFailure = { ok: false; reason: 'detached' | 'serialize-error' }\n\n/** Options for {@link createBridgeSyncPlugin}. */\nexport type BridgeSyncPluginOptions = {\n /** Called when `syncToSharedText` fails (excludes `reason: 'unchanged'`). */\n onSyncFailure?: (result: BridgeSyncFailure, view: EditorView) => void\n /** Called for non-fatal warnings. Default `console.warn`. */\n onWarning?: OnWarning\n}\n\n/** ProseMirror plugin key for {@link createBridgeSyncPlugin}. Use to read the plugin state. */\nexport const bridgeSyncPluginKey = new PluginKey<BridgeSyncState>('pm-cm-bridge-sync')\n\nconst wiredBridges = new WeakMap<YjsBridgeHandle, number>()\n\nconst defaultOnWarning: OnWarning = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`)\n\n/**\n * ProseMirror plugin that automatically syncs PM doc changes to Y.Text\n * via the bridge handle. Skips Yjs-originated changes to avoid loops.\n *\n * A warning is logged if the same bridge handle is wired more than once.\n * The guard is cleaned up when the plugin is destroyed.\n */\nexport function createBridgeSyncPlugin(\n bridge: YjsBridgeHandle,\n options: BridgeSyncPluginOptions = {},\n): Plugin {\n const warn = options.onWarning ?? defaultOnWarning\n\n // Tracks whether any Yjs-originated docChange was seen in the current\n // dispatch batch. Reset in view.update after each cycle.\n // appendTransaction plugins (e.g. prosemirror-tables) may emit follow-up\n // transactions that lack ySyncPlugin meta — this flag prevents those\n // from triggering a spurious text→PM→text round-trip.\n let yjsBatchSeen = false\n // Closure flag consumed in view.update — avoids sticky plugin state across dispatches.\n let needsSync = false\n\n return new Plugin<BridgeSyncState>({\n key: bridgeSyncPluginKey,\n\n state: {\n init(): BridgeSyncState {\n return { needsSync: false }\n },\n apply(tr, _prev): BridgeSyncState {\n if (!tr.docChanged) return { needsSync }\n if (bridge.isYjsSyncChange(tr)) {\n yjsBatchSeen = true\n needsSync = false\n return { needsSync: false }\n }\n if (yjsBatchSeen) {\n needsSync = false\n return { needsSync: false }\n }\n needsSync = true\n return { needsSync: true }\n },\n },\n\n view() {\n const count = wiredBridges.get(bridge) ?? 0\n if (count > 0) {\n warn({ code: 'bridge-already-wired', message: 'this bridge is already wired to another plugin instance' })\n }\n wiredBridges.set(bridge, count + 1)\n\n return {\n update(view) {\n if (needsSync) {\n const result = bridge.syncToSharedText(view.state.doc)\n if (!result.ok) {\n if (result.reason !== 'unchanged') {\n options.onSyncFailure?.(result, view)\n warn({ code: 'sync-failed', message: `bridge sync failed: ${result.reason}` })\n }\n }\n }\n needsSync = false\n yjsBatchSeen = false\n },\n destroy() {\n const remaining = (wiredBridges.get(bridge) ?? 1) - 1\n if (remaining <= 0) wiredBridges.delete(bridge)\n else wiredBridges.set(bridge, remaining)\n },\n }\n },\n })\n}\n","import { Plugin, PluginKey } from 'prosemirror-state'\nimport type { Node } from 'prosemirror-model'\nimport type { EditorView } from 'prosemirror-view'\nimport type { Awareness } from 'y-protocols/awareness'\nimport { absolutePositionToRelativePosition, ySyncPluginKey } from 'y-prosemirror'\nimport { createRelativePositionFromTypeIndex, createAbsolutePositionFromRelativePosition } from 'yjs'\nimport type { Text as YText, XmlFragment as YXmlFragment, RelativePosition } from 'yjs'\nimport type { Serialize, SerializeWithMap, CursorMap } from '@pm-cm/core'\nimport { buildCursorMap, cursorMapLookup, reverseCursorMapLookup } from '@pm-cm/core'\nimport type { OnWarning } from './types.js'\n\n/** Plugin state for the cursor sync plugin. Read via {@link cursorSyncPluginKey}. */\nexport type CursorSyncState = {\n /** Pending CodeMirror cursor to broadcast. Set by {@link syncCmCursor}. */\n pendingCm: { anchor: number; head: number } | null\n /** Text offset mapped from the current PM selection anchor. `null` when no mapping is available. */\n mappedTextOffset: number | null\n}\n\n/** ProseMirror plugin key for {@link createCursorSyncPlugin}. Use to read the plugin state. */\nexport const cursorSyncPluginKey = new PluginKey<CursorSyncState>('pm-cm-cursor-sync')\n\n/**\n * Internal shape of `ySyncPluginKey` state from y-prosemirror.\n * Not exported by upstream — kept here for explicit tracking.\n * Tested against y-prosemirror ^1.3.x.\n */\ntype YSyncPluginState = { type: YXmlFragment; binding: { mapping: Map<unknown, unknown> } }\n\nfunction getYSyncState(view: EditorView): YSyncPluginState | null {\n const raw = ySyncPluginKey.getState(view.state) as Record<string, unknown> | undefined\n if (!raw) return null\n if (\n typeof raw === 'object' &&\n 'type' in raw && raw.type &&\n 'binding' in raw && raw.binding &&\n typeof raw.binding === 'object' &&\n 'mapping' in (raw.binding as Record<string, unknown>) &&\n (raw.binding as Record<string, unknown>).mapping instanceof Map\n ) {\n return raw as unknown as YSyncPluginState\n }\n return null\n}\n\nfunction toRelativePosition(\n view: EditorView,\n pmPos: number,\n): unknown | null {\n const ySyncState = getYSyncState(view)\n if (!ySyncState) return null\n\n return absolutePositionToRelativePosition(\n pmPos,\n ySyncState.type,\n ySyncState.binding.mapping as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- y-prosemirror internal mapping type\n )\n}\n\n/** Returns `false` when ySyncPlugin state is unavailable (plugin not installed). */\nfunction broadcastPmCursor(\n awareness: Awareness,\n cursorFieldName: string,\n view: EditorView,\n pmAnchor: number,\n pmHead: number,\n): boolean {\n const relAnchor = toRelativePosition(view, pmAnchor)\n const relHead = toRelativePosition(view, pmHead)\n if (relAnchor === null || relHead === null) return false\n\n awareness.setLocalStateField(cursorFieldName, { anchor: relAnchor, head: relHead })\n return true\n}\n\nfunction broadcastTextCursor(\n awareness: Awareness,\n cmCursorFieldName: string,\n sharedText: YText,\n textAnchor: number,\n textHead: number,\n): void {\n const len = sharedText.length\n const clamp = (v: number) => Math.max(0, Math.min(v, len))\n const relAnchor = createRelativePositionFromTypeIndex(sharedText, clamp(textAnchor))\n const relHead = createRelativePositionFromTypeIndex(sharedText, clamp(textHead))\n awareness.setLocalStateField(cmCursorFieldName, { anchor: relAnchor, head: relHead })\n}\n\nconst defaultOnWarning: OnWarning = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`)\n\n/** Options for {@link createCursorSyncPlugin}. */\nexport type CursorSyncPluginOptions = {\n awareness: Awareness\n serialize: Serialize | SerializeWithMap\n cursorFieldName?: string\n /** Awareness field used for CM/Y.Text cursor payloads. Default `'cursor'`. */\n cmCursorFieldName?: string\n /**\n * When provided, the plugin also broadcasts CM-format cursor positions\n * (Y.Text relative positions) to the awareness field specified by\n * `cmCursorFieldName`, so that remote `yCollab` instances can render the cursor.\n */\n sharedText?: YText\n /** Called for non-fatal warnings. Default `console.warn`. */\n onWarning?: OnWarning\n}\n\n/**\n * ProseMirror plugin that synchronizes cursor positions between PM and CM via Yjs awareness.\n *\n * - PM → awareness: automatically broadcasts when the PM view is focused and selection changes.\n * - CM → awareness: triggered by dispatching {@link syncCmCursor}.\n */\nexport function createCursorSyncPlugin(options: CursorSyncPluginOptions): Plugin {\n const { awareness, serialize, sharedText } = options\n const warn = options.onWarning ?? defaultOnWarning\n const cursorFieldName = options.cursorFieldName ?? 'pmCursor'\n const cmCursorFieldName = options.cmCursorFieldName ?? 'cursor'\n\n if (sharedText && cursorFieldName === cmCursorFieldName) {\n throw new Error(\n `createCursorSyncPlugin: cursorFieldName and cmCursorFieldName must differ when sharedText is provided (both are \"${cursorFieldName}\")`,\n )\n }\n\n let warnedSyncPluginMissing = false\n\n // Closure variable consumed in view.update — survives appendTransaction\n // that would otherwise clear the plugin state before update() runs.\n let pendingCmCursor: { anchor: number; head: number } | null = null\n\n // Cached cursor map (serialize-based) — rebuilt only when doc changes\n let cachedMap: CursorMap | null = null\n let cachedMapDoc: Node | null = null\n\n function getOrBuildMap(doc: Node): CursorMap | null {\n if (cachedMapDoc !== doc || !cachedMap) {\n try {\n cachedMap = buildCursorMap(doc, serialize)\n } catch (error) {\n warn({ code: 'cursor-map-error', message: 'failed to build cursor map — cursor sync skipped' })\n cachedMap = null\n }\n cachedMapDoc = doc\n }\n return cachedMap\n }\n\n return new Plugin<CursorSyncState>({\n key: cursorSyncPluginKey,\n\n state: {\n init(): CursorSyncState {\n return { pendingCm: null, mappedTextOffset: null }\n },\n apply(tr, prev, _oldState, newState): CursorSyncState {\n const cmMeta = tr.getMeta(cursorSyncPluginKey) as\n | { anchor: number; head: number }\n | undefined\n if (cmMeta) {\n pendingCmCursor = cmMeta\n }\n\n // Compute PM → text offset when selection or doc changes\n let mappedTextOffset = prev.mappedTextOffset\n if (tr.selectionSet || tr.docChanged) {\n const map = getOrBuildMap(newState.doc)\n mappedTextOffset = map ? cursorMapLookup(map, newState.selection.anchor) : null\n }\n\n return {\n pendingCm: pendingCmCursor,\n mappedTextOffset,\n }\n },\n },\n\n view(editorView) {\n // Suppress awareness listener reactions when PM broadcasts to cmCursorFieldName\n let suppressCmReaction = false\n // Track last resolved CM cursor to avoid redundant pmCursor broadcasts\n let lastCmAbsAnchor = -1\n let lastCmAbsHead = -1\n // Set when the awareness listener already broadcast pmCursor for the\n // current CM cursor change, so pendingCm can skip its own broadcast.\n let cmCursorHandledByListener = false\n\n // When sharedText is available, yCollab writes the CM cursor (including\n // range selections) to awareness[cmCursorFieldName]. This listener\n // converts those Y.Text relative positions to PM positions and\n // broadcasts to pmCursor — so the app never needs to forward CM ranges.\n const handleAwarenessUpdate = (\n { added, updated }: { added: number[]; updated: number[]; removed: number[] },\n ) => {\n if (suppressCmReaction) return\n if (!sharedText?.doc) return\n const localId = awareness.clientID\n if (!updated.includes(localId) && !added.includes(localId)) return\n\n const localState = awareness.getLocalState() as Record<string, unknown> | null\n if (!localState) return\n\n const cmCursor = localState[cmCursorFieldName] as\n | { anchor: RelativePosition; head: RelativePosition }\n | undefined\n if (!cmCursor?.anchor || !cmCursor?.head) {\n // CM cursor was cleared — also clear pmCursor if it was previously set\n if (lastCmAbsAnchor !== -1) {\n awareness.setLocalStateField(cursorFieldName, null)\n lastCmAbsAnchor = -1\n lastCmAbsHead = -1\n }\n return\n }\n\n const absAnchor = createAbsolutePositionFromRelativePosition(cmCursor.anchor, sharedText.doc!)\n const absHead = createAbsolutePositionFromRelativePosition(cmCursor.head, sharedText.doc!)\n if (!absAnchor || !absHead) {\n // Relative position can't be resolved — clear stale PM cursor\n awareness.setLocalStateField(cursorFieldName, null)\n lastCmAbsAnchor = -1\n lastCmAbsHead = -1\n return\n }\n\n // Skip if unchanged (prevents loops from our own pmCursor broadcast)\n if (absAnchor.index === lastCmAbsAnchor && absHead.index === lastCmAbsHead) return\n\n const map = getOrBuildMap(editorView.state.doc)\n if (!map) {\n awareness.setLocalStateField(cursorFieldName, null)\n return\n }\n const pmAnchor = reverseCursorMapLookup(map, absAnchor.index)\n const pmHead = reverseCursorMapLookup(map, absHead.index)\n if (pmAnchor === null || pmHead === null) {\n // Mapping failed — clear stale PM cursor\n awareness.setLocalStateField(cursorFieldName, null)\n return\n }\n\n // Only update dedup cache after successful mapping — failed mapping\n // must not prevent retry when the doc changes and the map resolves.\n lastCmAbsAnchor = absAnchor.index\n lastCmAbsHead = absHead.index\n\n cmCursorHandledByListener = true\n const ok = broadcastPmCursor(awareness, cursorFieldName, editorView, pmAnchor, pmHead)\n if (!ok && !warnedSyncPluginMissing) {\n warnedSyncPluginMissing = true\n warn({ code: 'ysync-plugin-missing', message: 'ySyncPlugin state not available — cursor broadcast skipped' })\n }\n }\n\n if (sharedText) {\n awareness.on('update', handleAwarenessUpdate)\n }\n\n return {\n update(view, prevState) {\n // Invalidate dedup cache when doc changes so awareness listener\n // re-broadcasts pmCursor even if text offsets haven't changed.\n // Also re-trigger the handler to resolve cursors that failed mapping\n // when the doc was stale (no new awareness update would fire).\n if (view.state.doc !== prevState.doc) {\n lastCmAbsAnchor = -1\n lastCmAbsHead = -1\n if (sharedText?.doc && !suppressCmReaction) {\n handleAwarenessUpdate({ added: [awareness.clientID], updated: [], removed: [] })\n }\n }\n\n // CM → awareness: broadcast when pendingCmCursor is set.\n // Uses closure variable so appendTransaction cannot clear it.\n // If the awareness listener already converted yCollab's range to\n // pmCursor, skip here to avoid overwriting with collapsed data.\n if (pendingCmCursor != null) {\n const cursor = pendingCmCursor\n pendingCmCursor = null\n if (!cmCursorHandledByListener) {\n const map = getOrBuildMap(view.state.doc)\n const pmAnchor = map ? reverseCursorMapLookup(map, cursor.anchor) : null\n const pmHead = map ? reverseCursorMapLookup(map, cursor.head) : null\n if (pmAnchor !== null && pmHead !== null) {\n const ok = broadcastPmCursor(awareness, cursorFieldName, view, pmAnchor, pmHead)\n if (!ok && !warnedSyncPluginMissing) {\n warnedSyncPluginMissing = true\n warn({ code: 'ysync-plugin-missing', message: 'ySyncPlugin state not available — cursor broadcast skipped' })\n }\n // Also broadcast CM-format cursor so remote yCollab can render it.\n // (When yCollab is active the listener handles this; this path\n // covers the case where syncCmCursor is called without yCollab.)\n if (sharedText?.doc) {\n broadcastTextCursor(\n awareness,\n cmCursorFieldName,\n sharedText,\n cursor.anchor,\n cursor.head,\n )\n }\n } else {\n // Mapping failed — clear stale PM cursor\n awareness.setLocalStateField(cursorFieldName, null)\n }\n }\n cmCursorHandledByListener = false\n return\n }\n\n // PM → awareness: auto-broadcast on selection/doc change when focused.\n // suppressCmReaction wraps ALL awareness writes so the listener\n // does not echo stale CM cursor back to pmCursor.\n if (\n view.hasFocus() &&\n (view.state.selection !== prevState.selection ||\n view.state.doc !== prevState.doc)\n ) {\n suppressCmReaction = true\n try {\n const { anchor, head } = view.state.selection\n const ok = broadcastPmCursor(awareness, cursorFieldName, view, anchor, head)\n if (!ok && !warnedSyncPluginMissing) {\n warnedSyncPluginMissing = true\n warn({ code: 'ysync-plugin-missing', message: 'ySyncPlugin state not available — cursor broadcast skipped' })\n }\n // Also broadcast CM-format cursor so remote yCollab can render it.\n if (sharedText?.doc) {\n const map = getOrBuildMap(view.state.doc)\n const textAnchor = map ? cursorMapLookup(map, anchor) : null\n const textHead = map ? cursorMapLookup(map, head) : null\n if (textAnchor !== null && textHead !== null) {\n broadcastTextCursor(awareness, cmCursorFieldName, sharedText, textAnchor, textHead)\n } else {\n // Mapping failed — clear stale CM cursor\n awareness.setLocalStateField(cmCursorFieldName, null)\n }\n }\n } finally {\n suppressCmReaction = false\n }\n }\n\n // Always reset so a stale flag from a previous awareness update\n // cannot suppress a later syncCmCursor broadcast.\n cmCursorHandledByListener = false\n },\n destroy() {\n // Clear cursor fields so remote clients don't see ghost cursors\n awareness.setLocalStateField(cursorFieldName, null)\n if (sharedText) {\n awareness.setLocalStateField(cmCursorFieldName, null)\n awareness.off('update', handleAwarenessUpdate)\n }\n },\n }\n },\n })\n}\n\n/**\n * Dispatch a CodeMirror cursor offset (or range) to the cursor sync plugin.\n * The plugin will convert it to a ProseMirror position and broadcast via awareness.\n *\n * @param view - The ProseMirror EditorView that has the cursor sync plugin installed.\n * @param anchor - CodeMirror text offset for the anchor.\n * @param head - CodeMirror text offset for the head (defaults to `anchor` for a collapsed cursor).\n * @param onWarning - Optional warning callback. Default `console.warn`.\n */\nexport function syncCmCursor(view: EditorView, anchor: number, head?: number, onWarning?: OnWarning): void {\n if (!cursorSyncPluginKey.getState(view.state)) {\n (onWarning ?? defaultOnWarning)({ code: 'cursor-sync-not-installed', message: 'cursor sync plugin is not installed on this EditorView' })\n return\n }\n const sanitize = (v: number) => { const n = Math.floor(v); return Number.isFinite(n) ? Math.max(0, n) : 0 }\n view.dispatch(\n view.state.tr.setMeta(cursorSyncPluginKey, {\n anchor: sanitize(anchor),\n head: sanitize(head ?? anchor),\n }),\n )\n}\n"],"mappings":"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","names":["doc","value","import_y_prosemirror","import_prosemirror_state","import_y_prosemirror","defaultOnWarning","import_core"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -7,7 +7,7 @@ import { Awareness } from 'y-protocols/awareness';
|
|
|
7
7
|
import { DecorationAttrs, EditorView } from 'prosemirror-view';
|
|
8
8
|
|
|
9
9
|
/** Known warning codes emitted by the yjs bridge and plugins. */
|
|
10
|
-
type WarningCode = 'bridge-already-wired' | 'sync-failed' | 'ysync-plugin-missing' | 'cursor-sync-not-installed';
|
|
10
|
+
type WarningCode = 'bridge-already-wired' | 'sync-failed' | 'ysync-plugin-missing' | 'cursor-sync-not-installed' | 'cursor-map-error';
|
|
11
11
|
/** Structured warning event for non-fatal warnings. */
|
|
12
12
|
type WarningEvent = {
|
|
13
13
|
code: WarningCode;
|
|
@@ -21,6 +21,7 @@ type WarningEvent = {
|
|
|
21
21
|
* - `'sync-failed'` — `syncToSharedText` failed (e.g. Y.Text detached).
|
|
22
22
|
* - `'ysync-plugin-missing'` — ySyncPlugin state is not available; cursor broadcast skipped.
|
|
23
23
|
* - `'cursor-sync-not-installed'` — cursor sync plugin is not installed on the EditorView.
|
|
24
|
+
* - `'cursor-map-error'` — failed to build cursor map (serialize threw); cursor sync skipped.
|
|
24
25
|
*/
|
|
25
26
|
type OnWarning = (event: WarningEvent) => void;
|
|
26
27
|
/** Yjs transaction origin: text → ProseMirror direction. */
|
|
@@ -71,6 +72,9 @@ type ReplaceTextResult = {
|
|
|
71
72
|
} | {
|
|
72
73
|
ok: false;
|
|
73
74
|
reason: 'detached';
|
|
75
|
+
} | {
|
|
76
|
+
ok: false;
|
|
77
|
+
reason: 'serialize-error';
|
|
74
78
|
};
|
|
75
79
|
/** Result of {@link replaceSharedProseMirror}. */
|
|
76
80
|
type ReplaceProseMirrorResult = {
|
|
@@ -197,7 +201,7 @@ type BridgeSyncState = {
|
|
|
197
201
|
};
|
|
198
202
|
type BridgeSyncFailure = {
|
|
199
203
|
ok: false;
|
|
200
|
-
reason: 'detached';
|
|
204
|
+
reason: 'detached' | 'serialize-error';
|
|
201
205
|
};
|
|
202
206
|
/** Options for {@link createBridgeSyncPlugin}. */
|
|
203
207
|
type BridgeSyncPluginOptions = {
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { Awareness } from 'y-protocols/awareness';
|
|
|
7
7
|
import { DecorationAttrs, EditorView } from 'prosemirror-view';
|
|
8
8
|
|
|
9
9
|
/** Known warning codes emitted by the yjs bridge and plugins. */
|
|
10
|
-
type WarningCode = 'bridge-already-wired' | 'sync-failed' | 'ysync-plugin-missing' | 'cursor-sync-not-installed';
|
|
10
|
+
type WarningCode = 'bridge-already-wired' | 'sync-failed' | 'ysync-plugin-missing' | 'cursor-sync-not-installed' | 'cursor-map-error';
|
|
11
11
|
/** Structured warning event for non-fatal warnings. */
|
|
12
12
|
type WarningEvent = {
|
|
13
13
|
code: WarningCode;
|
|
@@ -21,6 +21,7 @@ type WarningEvent = {
|
|
|
21
21
|
* - `'sync-failed'` — `syncToSharedText` failed (e.g. Y.Text detached).
|
|
22
22
|
* - `'ysync-plugin-missing'` — ySyncPlugin state is not available; cursor broadcast skipped.
|
|
23
23
|
* - `'cursor-sync-not-installed'` — cursor sync plugin is not installed on the EditorView.
|
|
24
|
+
* - `'cursor-map-error'` — failed to build cursor map (serialize threw); cursor sync skipped.
|
|
24
25
|
*/
|
|
25
26
|
type OnWarning = (event: WarningEvent) => void;
|
|
26
27
|
/** Yjs transaction origin: text → ProseMirror direction. */
|
|
@@ -71,6 +72,9 @@ type ReplaceTextResult = {
|
|
|
71
72
|
} | {
|
|
72
73
|
ok: false;
|
|
73
74
|
reason: 'detached';
|
|
75
|
+
} | {
|
|
76
|
+
ok: false;
|
|
77
|
+
reason: 'serialize-error';
|
|
74
78
|
};
|
|
75
79
|
/** Result of {@link replaceSharedProseMirror}. */
|
|
76
80
|
type ReplaceProseMirrorResult = {
|
|
@@ -197,7 +201,7 @@ type BridgeSyncState = {
|
|
|
197
201
|
};
|
|
198
202
|
type BridgeSyncFailure = {
|
|
199
203
|
ok: false;
|
|
200
|
-
reason: 'detached';
|
|
204
|
+
reason: 'detached' | 'serialize-error';
|
|
201
205
|
};
|
|
202
206
|
/** Options for {@link createBridgeSyncPlugin}. */
|
|
203
207
|
type BridgeSyncPluginOptions = {
|
package/dist/index.js
CHANGED
|
@@ -128,7 +128,13 @@ function createYjsBridge(config, options) {
|
|
|
128
128
|
return { source: "initial", parseError: true };
|
|
129
129
|
}
|
|
130
130
|
const pmDoc = yXmlFragmentToProseMirrorRootNode(sharedProseMirror, schema);
|
|
131
|
-
|
|
131
|
+
let canonicalText;
|
|
132
|
+
try {
|
|
133
|
+
canonicalText = serialize(pmDoc);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
onError({ code: "serialize-error", message: "failed to serialize ProseMirror document during bootstrap", cause: error });
|
|
136
|
+
return { source: "initial", parseError: true };
|
|
137
|
+
}
|
|
132
138
|
replaceSharedText(sharedText, canonicalText, ORIGIN_INIT, normalize);
|
|
133
139
|
lastBridgedText = normalize(canonicalText);
|
|
134
140
|
return { source: "initial" };
|
|
@@ -190,7 +196,13 @@ function createYjsBridge(config, options) {
|
|
|
190
196
|
return {
|
|
191
197
|
bootstrapResult,
|
|
192
198
|
syncToSharedText(doc2) {
|
|
193
|
-
|
|
199
|
+
let text;
|
|
200
|
+
try {
|
|
201
|
+
text = serialize(doc2);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
onError({ code: "serialize-error", message: "failed to serialize ProseMirror document", cause: error });
|
|
204
|
+
return { ok: false, reason: "serialize-error" };
|
|
205
|
+
}
|
|
194
206
|
const result = replaceSharedText(sharedText, text, ORIGIN_PM_TO_TEXT, normalize);
|
|
195
207
|
if (result.ok || result.reason === "unchanged") {
|
|
196
208
|
lastBridgedText = normalize(text);
|
|
@@ -228,10 +240,12 @@ function createAwarenessProxy(awareness, cursorField = "pmCursor") {
|
|
|
228
240
|
const states = target.getStates();
|
|
229
241
|
const remapped = /* @__PURE__ */ new Map();
|
|
230
242
|
states.forEach((state, clientId) => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
cursor:
|
|
234
|
-
}
|
|
243
|
+
const s = state;
|
|
244
|
+
if (cursorField in s) {
|
|
245
|
+
remapped.set(clientId, { ...s, cursor: s[cursorField] ?? null });
|
|
246
|
+
} else {
|
|
247
|
+
remapped.set(clientId, s);
|
|
248
|
+
}
|
|
235
249
|
});
|
|
236
250
|
return remapped;
|
|
237
251
|
};
|
|
@@ -248,14 +262,12 @@ import { initProseMirrorDoc, yCursorPlugin, ySyncPlugin, yUndoPlugin } from "y-p
|
|
|
248
262
|
// src/bridge-sync-plugin.ts
|
|
249
263
|
import { Plugin, PluginKey } from "prosemirror-state";
|
|
250
264
|
var bridgeSyncPluginKey = new PluginKey("pm-cm-bridge-sync");
|
|
251
|
-
var wiredBridges = /* @__PURE__ */ new
|
|
265
|
+
var wiredBridges = /* @__PURE__ */ new WeakMap();
|
|
252
266
|
var defaultOnWarning = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`);
|
|
253
267
|
function createBridgeSyncPlugin(bridge, options = {}) {
|
|
254
268
|
const warn = options.onWarning ?? defaultOnWarning;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
wiredBridges.add(bridge);
|
|
269
|
+
let yjsBatchSeen = false;
|
|
270
|
+
let needsSync = false;
|
|
259
271
|
return new Plugin({
|
|
260
272
|
key: bridgeSyncPluginKey,
|
|
261
273
|
state: {
|
|
@@ -263,27 +275,44 @@ function createBridgeSyncPlugin(bridge, options = {}) {
|
|
|
263
275
|
return { needsSync: false };
|
|
264
276
|
},
|
|
265
277
|
apply(tr, _prev) {
|
|
266
|
-
if (!tr.docChanged) return { needsSync
|
|
267
|
-
if (bridge.isYjsSyncChange(tr))
|
|
278
|
+
if (!tr.docChanged) return { needsSync };
|
|
279
|
+
if (bridge.isYjsSyncChange(tr)) {
|
|
280
|
+
yjsBatchSeen = true;
|
|
281
|
+
needsSync = false;
|
|
282
|
+
return { needsSync: false };
|
|
283
|
+
}
|
|
284
|
+
if (yjsBatchSeen) {
|
|
285
|
+
needsSync = false;
|
|
286
|
+
return { needsSync: false };
|
|
287
|
+
}
|
|
288
|
+
needsSync = true;
|
|
268
289
|
return { needsSync: true };
|
|
269
290
|
}
|
|
270
291
|
},
|
|
271
292
|
view() {
|
|
293
|
+
const count = wiredBridges.get(bridge) ?? 0;
|
|
294
|
+
if (count > 0) {
|
|
295
|
+
warn({ code: "bridge-already-wired", message: "this bridge is already wired to another plugin instance" });
|
|
296
|
+
}
|
|
297
|
+
wiredBridges.set(bridge, count + 1);
|
|
272
298
|
return {
|
|
273
299
|
update(view) {
|
|
274
|
-
|
|
275
|
-
if (state?.needsSync) {
|
|
300
|
+
if (needsSync) {
|
|
276
301
|
const result = bridge.syncToSharedText(view.state.doc);
|
|
277
302
|
if (!result.ok) {
|
|
278
|
-
if (result.reason
|
|
303
|
+
if (result.reason !== "unchanged") {
|
|
279
304
|
options.onSyncFailure?.(result, view);
|
|
280
305
|
warn({ code: "sync-failed", message: `bridge sync failed: ${result.reason}` });
|
|
281
306
|
}
|
|
282
307
|
}
|
|
283
308
|
}
|
|
309
|
+
needsSync = false;
|
|
310
|
+
yjsBatchSeen = false;
|
|
284
311
|
},
|
|
285
312
|
destroy() {
|
|
286
|
-
wiredBridges.
|
|
313
|
+
const remaining = (wiredBridges.get(bridge) ?? 1) - 1;
|
|
314
|
+
if (remaining <= 0) wiredBridges.delete(bridge);
|
|
315
|
+
else wiredBridges.set(bridge, remaining);
|
|
287
316
|
}
|
|
288
317
|
};
|
|
289
318
|
}
|
|
@@ -293,7 +322,7 @@ function createBridgeSyncPlugin(bridge, options = {}) {
|
|
|
293
322
|
// src/cursor-sync-plugin.ts
|
|
294
323
|
import { Plugin as Plugin2, PluginKey as PluginKey2 } from "prosemirror-state";
|
|
295
324
|
import { absolutePositionToRelativePosition, ySyncPluginKey as ySyncPluginKey2 } from "y-prosemirror";
|
|
296
|
-
import { createRelativePositionFromTypeIndex } from "yjs";
|
|
325
|
+
import { createRelativePositionFromTypeIndex, createAbsolutePositionFromRelativePosition } from "yjs";
|
|
297
326
|
import { buildCursorMap, cursorMapLookup, reverseCursorMapLookup } from "@pm-cm/core";
|
|
298
327
|
var cursorSyncPluginKey = new PluginKey2("pm-cm-cursor-sync");
|
|
299
328
|
function getYSyncState(view) {
|
|
@@ -334,12 +363,23 @@ function createCursorSyncPlugin(options) {
|
|
|
334
363
|
const warn = options.onWarning ?? defaultOnWarning2;
|
|
335
364
|
const cursorFieldName = options.cursorFieldName ?? "pmCursor";
|
|
336
365
|
const cmCursorFieldName = options.cmCursorFieldName ?? "cursor";
|
|
366
|
+
if (sharedText && cursorFieldName === cmCursorFieldName) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`createCursorSyncPlugin: cursorFieldName and cmCursorFieldName must differ when sharedText is provided (both are "${cursorFieldName}")`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
337
371
|
let warnedSyncPluginMissing = false;
|
|
372
|
+
let pendingCmCursor = null;
|
|
338
373
|
let cachedMap = null;
|
|
339
374
|
let cachedMapDoc = null;
|
|
340
375
|
function getOrBuildMap(doc) {
|
|
341
376
|
if (cachedMapDoc !== doc || !cachedMap) {
|
|
342
|
-
|
|
377
|
+
try {
|
|
378
|
+
cachedMap = buildCursorMap(doc, serialize);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
warn({ code: "cursor-map-error", message: "failed to build cursor map \u2014 cursor sync skipped" });
|
|
381
|
+
cachedMap = null;
|
|
382
|
+
}
|
|
343
383
|
cachedMapDoc = doc;
|
|
344
384
|
}
|
|
345
385
|
return cachedMap;
|
|
@@ -353,62 +393,141 @@ function createCursorSyncPlugin(options) {
|
|
|
353
393
|
apply(tr, prev, _oldState, newState) {
|
|
354
394
|
const cmMeta = tr.getMeta(cursorSyncPluginKey);
|
|
355
395
|
if (cmMeta) {
|
|
356
|
-
|
|
396
|
+
pendingCmCursor = cmMeta;
|
|
357
397
|
}
|
|
358
398
|
let mappedTextOffset = prev.mappedTextOffset;
|
|
359
399
|
if (tr.selectionSet || tr.docChanged) {
|
|
360
400
|
const map = getOrBuildMap(newState.doc);
|
|
361
|
-
mappedTextOffset = cursorMapLookup(map, newState.selection.anchor);
|
|
401
|
+
mappedTextOffset = map ? cursorMapLookup(map, newState.selection.anchor) : null;
|
|
362
402
|
}
|
|
363
403
|
return {
|
|
364
|
-
pendingCm:
|
|
404
|
+
pendingCm: pendingCmCursor,
|
|
365
405
|
mappedTextOffset
|
|
366
406
|
};
|
|
367
407
|
}
|
|
368
408
|
},
|
|
369
|
-
view() {
|
|
409
|
+
view(editorView) {
|
|
410
|
+
let suppressCmReaction = false;
|
|
411
|
+
let lastCmAbsAnchor = -1;
|
|
412
|
+
let lastCmAbsHead = -1;
|
|
413
|
+
let cmCursorHandledByListener = false;
|
|
414
|
+
const handleAwarenessUpdate = ({ added, updated }) => {
|
|
415
|
+
if (suppressCmReaction) return;
|
|
416
|
+
if (!sharedText?.doc) return;
|
|
417
|
+
const localId = awareness.clientID;
|
|
418
|
+
if (!updated.includes(localId) && !added.includes(localId)) return;
|
|
419
|
+
const localState = awareness.getLocalState();
|
|
420
|
+
if (!localState) return;
|
|
421
|
+
const cmCursor = localState[cmCursorFieldName];
|
|
422
|
+
if (!cmCursor?.anchor || !cmCursor?.head) {
|
|
423
|
+
if (lastCmAbsAnchor !== -1) {
|
|
424
|
+
awareness.setLocalStateField(cursorFieldName, null);
|
|
425
|
+
lastCmAbsAnchor = -1;
|
|
426
|
+
lastCmAbsHead = -1;
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const absAnchor = createAbsolutePositionFromRelativePosition(cmCursor.anchor, sharedText.doc);
|
|
431
|
+
const absHead = createAbsolutePositionFromRelativePosition(cmCursor.head, sharedText.doc);
|
|
432
|
+
if (!absAnchor || !absHead) {
|
|
433
|
+
awareness.setLocalStateField(cursorFieldName, null);
|
|
434
|
+
lastCmAbsAnchor = -1;
|
|
435
|
+
lastCmAbsHead = -1;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (absAnchor.index === lastCmAbsAnchor && absHead.index === lastCmAbsHead) return;
|
|
439
|
+
const map = getOrBuildMap(editorView.state.doc);
|
|
440
|
+
if (!map) {
|
|
441
|
+
awareness.setLocalStateField(cursorFieldName, null);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const pmAnchor = reverseCursorMapLookup(map, absAnchor.index);
|
|
445
|
+
const pmHead = reverseCursorMapLookup(map, absHead.index);
|
|
446
|
+
if (pmAnchor === null || pmHead === null) {
|
|
447
|
+
awareness.setLocalStateField(cursorFieldName, null);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
lastCmAbsAnchor = absAnchor.index;
|
|
451
|
+
lastCmAbsHead = absHead.index;
|
|
452
|
+
cmCursorHandledByListener = true;
|
|
453
|
+
const ok = broadcastPmCursor(awareness, cursorFieldName, editorView, pmAnchor, pmHead);
|
|
454
|
+
if (!ok && !warnedSyncPluginMissing) {
|
|
455
|
+
warnedSyncPluginMissing = true;
|
|
456
|
+
warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
if (sharedText) {
|
|
460
|
+
awareness.on("update", handleAwarenessUpdate);
|
|
461
|
+
}
|
|
370
462
|
return {
|
|
371
463
|
update(view, prevState) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const pmHead = reverseCursorMapLookup(map, pluginState.pendingCm.head);
|
|
378
|
-
if (pmAnchor !== null && pmHead !== null) {
|
|
379
|
-
const ok = broadcastPmCursor(awareness, cursorFieldName, view, pmAnchor, pmHead);
|
|
380
|
-
if (!ok && !warnedSyncPluginMissing) {
|
|
381
|
-
warnedSyncPluginMissing = true;
|
|
382
|
-
warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
|
|
383
|
-
}
|
|
464
|
+
if (view.state.doc !== prevState.doc) {
|
|
465
|
+
lastCmAbsAnchor = -1;
|
|
466
|
+
lastCmAbsHead = -1;
|
|
467
|
+
if (sharedText?.doc && !suppressCmReaction) {
|
|
468
|
+
handleAwarenessUpdate({ added: [awareness.clientID], updated: [], removed: [] });
|
|
384
469
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
);
|
|
470
|
+
}
|
|
471
|
+
if (pendingCmCursor != null) {
|
|
472
|
+
const cursor = pendingCmCursor;
|
|
473
|
+
pendingCmCursor = null;
|
|
474
|
+
if (!cmCursorHandledByListener) {
|
|
475
|
+
const map = getOrBuildMap(view.state.doc);
|
|
476
|
+
const pmAnchor = map ? reverseCursorMapLookup(map, cursor.anchor) : null;
|
|
477
|
+
const pmHead = map ? reverseCursorMapLookup(map, cursor.head) : null;
|
|
478
|
+
if (pmAnchor !== null && pmHead !== null) {
|
|
479
|
+
const ok = broadcastPmCursor(awareness, cursorFieldName, view, pmAnchor, pmHead);
|
|
480
|
+
if (!ok && !warnedSyncPluginMissing) {
|
|
481
|
+
warnedSyncPluginMissing = true;
|
|
482
|
+
warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
|
|
483
|
+
}
|
|
484
|
+
if (sharedText?.doc) {
|
|
485
|
+
broadcastTextCursor(
|
|
486
|
+
awareness,
|
|
487
|
+
cmCursorFieldName,
|
|
488
|
+
sharedText,
|
|
489
|
+
cursor.anchor,
|
|
490
|
+
cursor.head
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
awareness.setLocalStateField(cursorFieldName, null);
|
|
495
|
+
}
|
|
393
496
|
}
|
|
497
|
+
cmCursorHandledByListener = false;
|
|
394
498
|
return;
|
|
395
499
|
}
|
|
396
500
|
if (view.hasFocus() && (view.state.selection !== prevState.selection || view.state.doc !== prevState.doc)) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
501
|
+
suppressCmReaction = true;
|
|
502
|
+
try {
|
|
503
|
+
const { anchor, head } = view.state.selection;
|
|
504
|
+
const ok = broadcastPmCursor(awareness, cursorFieldName, view, anchor, head);
|
|
505
|
+
if (!ok && !warnedSyncPluginMissing) {
|
|
506
|
+
warnedSyncPluginMissing = true;
|
|
507
|
+
warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
|
|
508
|
+
}
|
|
509
|
+
if (sharedText?.doc) {
|
|
510
|
+
const map = getOrBuildMap(view.state.doc);
|
|
511
|
+
const textAnchor = map ? cursorMapLookup(map, anchor) : null;
|
|
512
|
+
const textHead = map ? cursorMapLookup(map, head) : null;
|
|
513
|
+
if (textAnchor !== null && textHead !== null) {
|
|
514
|
+
broadcastTextCursor(awareness, cmCursorFieldName, sharedText, textAnchor, textHead);
|
|
515
|
+
} else {
|
|
516
|
+
awareness.setLocalStateField(cmCursorFieldName, null);
|
|
517
|
+
}
|
|
409
518
|
}
|
|
519
|
+
} finally {
|
|
520
|
+
suppressCmReaction = false;
|
|
410
521
|
}
|
|
411
522
|
}
|
|
523
|
+
cmCursorHandledByListener = false;
|
|
524
|
+
},
|
|
525
|
+
destroy() {
|
|
526
|
+
awareness.setLocalStateField(cursorFieldName, null);
|
|
527
|
+
if (sharedText) {
|
|
528
|
+
awareness.setLocalStateField(cmCursorFieldName, null);
|
|
529
|
+
awareness.off("update", handleAwarenessUpdate);
|
|
530
|
+
}
|
|
412
531
|
}
|
|
413
532
|
};
|
|
414
533
|
}
|
|
@@ -419,7 +538,10 @@ function syncCmCursor(view, anchor, head, onWarning) {
|
|
|
419
538
|
(onWarning ?? defaultOnWarning2)({ code: "cursor-sync-not-installed", message: "cursor sync plugin is not installed on this EditorView" });
|
|
420
539
|
return;
|
|
421
540
|
}
|
|
422
|
-
const sanitize = (v) =>
|
|
541
|
+
const sanitize = (v) => {
|
|
542
|
+
const n = Math.floor(v);
|
|
543
|
+
return Number.isFinite(n) ? Math.max(0, n) : 0;
|
|
544
|
+
};
|
|
423
545
|
view.dispatch(
|
|
424
546
|
view.state.tr.setMeta(cursorSyncPluginKey, {
|
|
425
547
|
anchor: sanitize(anchor),
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/bridge.ts","../src/types.ts","../src/awareness-proxy.ts","../src/collab-plugins.ts","../src/bridge-sync-plugin.ts","../src/cursor-sync-plugin.ts","../src/index.ts"],"sourcesContent":["import type { Node } from 'prosemirror-model'\nimport type { Transaction } from 'prosemirror-state'\nimport { prosemirrorToYXmlFragment, yXmlFragmentToProseMirrorRootNode, ySyncPluginKey } from 'y-prosemirror'\nimport type { Doc, Text as YText, XmlFragment as YXmlFragment } from 'yjs'\nimport type { Normalize, OnError } from '@pm-cm/core'\nimport type { BootstrapResult, YjsBridgeConfig, YjsBridgeHandle } from './types.js'\nimport { ORIGIN_INIT, ORIGIN_TEXT_TO_PM, ORIGIN_PM_TO_TEXT } from './types.js'\n\nconst defaultNormalize: Normalize = (s) => s.replace(/\\r\\n?/g, '\\n')\nconst defaultOnError: OnError = (event) => console.error(`[bridge] ${event.code}: ${event.message}`, event.cause)\n\n/** Result of {@link replaceSharedText}. */\nexport type ReplaceTextResult =\n | { ok: true }\n | { ok: false; reason: 'unchanged' }\n | { ok: false; reason: 'detached' }\n\n/** Result of {@link replaceSharedProseMirror}. */\nexport type ReplaceProseMirrorResult =\n | { ok: true }\n | { ok: false; reason: 'parse-error' }\n | { ok: false; reason: 'detached' }\n\n/**\n * Union of all replace-result types. Kept for backward compatibility.\n * Prefer the narrower {@link ReplaceTextResult} / {@link ReplaceProseMirrorResult}.\n */\nexport type ReplaceResult = ReplaceTextResult | ReplaceProseMirrorResult\n\n/**\n * Replace `Y.Text` content using a minimal diff (common prefix/suffix trimming).\n * Returns a {@link ReplaceResult} indicating success or failure reason.\n */\nexport function replaceSharedText(\n sharedText: YText,\n next: string,\n origin: unknown,\n normalize: Normalize = defaultNormalize,\n): ReplaceTextResult {\n if (!sharedText.doc) {\n return { ok: false, reason: 'detached' }\n }\n\n const normalized = normalize(next)\n const current = sharedText.toString()\n if (current === normalized) {\n return { ok: false, reason: 'unchanged' }\n }\n\n // Minimal diff: find common prefix and suffix, replace only the changed middle.\n let start = 0\n const minLen = Math.min(current.length, normalized.length)\n while (start < minLen && current.charCodeAt(start) === normalized.charCodeAt(start)) {\n start++\n }\n\n let endCurrent = current.length\n let endNext = normalized.length\n while (endCurrent > start && endNext > start && current.charCodeAt(endCurrent - 1) === normalized.charCodeAt(endNext - 1)) {\n endCurrent--\n endNext--\n }\n\n sharedText.doc.transact(() => {\n const deleteCount = endCurrent - start\n if (deleteCount > 0) {\n sharedText.delete(start, deleteCount)\n }\n const insertStr = normalized.slice(start, endNext)\n if (insertStr.length > 0) {\n sharedText.insert(start, insertStr)\n }\n }, origin)\n\n return { ok: true }\n}\n\n/**\n * Replace `Y.XmlFragment` by parsing serialized text into a ProseMirror document.\n * Returns a {@link ReplaceResult} indicating success or failure reason.\n */\nexport function replaceSharedProseMirror(\n doc: Doc,\n fragment: YXmlFragment,\n text: string,\n origin: unknown,\n config: Pick<YjsBridgeConfig, 'schema' | 'parse' | 'normalize' | 'onError'>,\n): ReplaceProseMirrorResult {\n if (!fragment.doc) {\n return { ok: false, reason: 'detached' }\n }\n if (fragment.doc !== doc) {\n throw new Error('fragment belongs to a different Y.Doc than the provided doc')\n }\n\n const normalize = config.normalize ?? defaultNormalize\n const onError = config.onError ?? defaultOnError\n let nextDoc: Node\n try {\n nextDoc = config.parse(normalize(text), config.schema)\n } catch (error) {\n onError({ code: 'parse-error', message: 'failed to parse text into ProseMirror document', cause: error })\n return { ok: false, reason: 'parse-error' }\n }\n\n doc.transact(() => {\n prosemirrorToYXmlFragment(nextDoc, fragment)\n }, origin)\n return { ok: true }\n}\n\n/** Options for {@link createYjsBridge}. */\nexport type YjsBridgeOptions = {\n initialText?: string\n /** Which side wins when both sharedText and sharedProseMirror exist and differ. Default `'text'`. */\n prefer?: 'text' | 'prosemirror'\n}\n\n/**\n * Create a collaborative bridge that keeps `Y.Text` and `Y.XmlFragment` in sync.\n *\n * Runs a synchronous bootstrap to reconcile existing state, then installs a\n * `Y.Text` observer for the text → ProseMirror direction.\n *\n * @throws If `sharedText` or `sharedProseMirror` belong to a different `Y.Doc`.\n */\nexport function createYjsBridge(\n config: YjsBridgeConfig,\n options?: YjsBridgeOptions,\n): YjsBridgeHandle {\n const {\n doc,\n sharedText,\n sharedProseMirror,\n schema,\n serialize,\n parse,\n } = config\n const normalize = config.normalize ?? defaultNormalize\n const onError = config.onError ?? defaultOnError\n\n if (!sharedText.doc) {\n throw new Error('sharedText is not attached to any Y.Doc')\n }\n if (sharedText.doc !== doc) {\n throw new Error('sharedText belongs to a different Y.Doc than the provided doc')\n }\n if (!sharedProseMirror.doc) {\n throw new Error('sharedProseMirror is not attached to any Y.Doc')\n }\n if (sharedProseMirror.doc !== doc) {\n throw new Error('sharedProseMirror belongs to a different Y.Doc than the provided doc')\n }\n\n let lastBridgedText: string | null = null\n\n /** Returns `true` if the parse succeeded. */\n const syncTextToProsemirror = (origin: unknown): boolean => {\n const text = normalize(sharedText.toString())\n if (lastBridgedText === text) {\n return true\n }\n\n const result = replaceSharedProseMirror(doc, sharedProseMirror, text, origin, {\n schema,\n parse,\n normalize,\n onError,\n })\n if (result.ok) {\n lastBridgedText = text\n }\n return result.ok\n }\n\n const sharedProseMirrorToText = (fragment: YXmlFragment): string | null => {\n try {\n const pmDoc = yXmlFragmentToProseMirrorRootNode(fragment, schema)\n return normalize(serialize(pmDoc))\n } catch (error) {\n onError({ code: 'serialize-error', message: 'failed to convert ProseMirror fragment to text', cause: error })\n return null\n }\n }\n\n // Bootstrap\n const bootstrap = (): BootstrapResult => {\n const text = normalize(sharedText.toString())\n const hasText = text.length > 0\n const hasProsemirror = sharedProseMirror.length > 0\n\n if (!hasText && !hasProsemirror) {\n const initial = options?.initialText ?? ''\n if (initial.length > 0) {\n // Set Y.XmlFragment first, then derive Y.Text from serialize(parse(initial))\n // to ensure both shared types are in the same canonical form.\n const initResult = replaceSharedProseMirror(doc, sharedProseMirror, initial, ORIGIN_INIT, {\n schema,\n parse,\n normalize,\n onError,\n })\n if (!initResult.ok) {\n return { source: 'initial', parseError: true }\n }\n const pmDoc = yXmlFragmentToProseMirrorRootNode(sharedProseMirror, schema)\n const canonicalText = serialize(pmDoc)\n replaceSharedText(sharedText, canonicalText, ORIGIN_INIT, normalize)\n lastBridgedText = normalize(canonicalText)\n return { source: 'initial' }\n }\n return { source: 'empty' }\n }\n\n if (hasText && !hasProsemirror) {\n const ok = syncTextToProsemirror(ORIGIN_INIT)\n return { source: 'text', ...(!ok && { parseError: true }) }\n }\n\n if (!hasText && hasProsemirror) {\n const textFromProsemirror = sharedProseMirrorToText(sharedProseMirror)\n if (textFromProsemirror !== null) {\n replaceSharedText(sharedText, textFromProsemirror, ORIGIN_INIT, normalize)\n lastBridgedText = normalize(textFromProsemirror)\n return { source: 'prosemirror' }\n }\n return { source: 'prosemirror', parseError: true }\n }\n\n const prosemirrorText = sharedProseMirrorToText(sharedProseMirror)\n if (prosemirrorText === null) {\n const fallbackText = hasText ? text : (options?.initialText ?? '')\n let parseError = false\n if (fallbackText.length > 0) {\n replaceSharedText(sharedText, fallbackText, ORIGIN_INIT, normalize)\n const fallbackResult = replaceSharedProseMirror(doc, sharedProseMirror, fallbackText, ORIGIN_INIT, {\n schema,\n parse,\n normalize,\n onError,\n })\n if (!fallbackResult.ok) parseError = true\n }\n return { source: 'text', ...(parseError && { parseError: true }) }\n }\n\n if (prosemirrorText !== text) {\n const prefer = options?.prefer ?? 'text'\n if (prefer === 'prosemirror') {\n replaceSharedText(sharedText, prosemirrorText, ORIGIN_INIT, normalize)\n lastBridgedText = normalize(prosemirrorText)\n return { source: 'prosemirror' }\n } else {\n const ok = syncTextToProsemirror(ORIGIN_INIT)\n return { source: 'text', ...(!ok && { parseError: true }) }\n }\n } else {\n lastBridgedText = text\n return { source: 'both-match' }\n }\n }\n\n const textObserver = (\n _: unknown,\n transaction: { origin: unknown },\n ) => {\n if (transaction.origin === ORIGIN_PM_TO_TEXT || transaction.origin === ORIGIN_INIT) {\n return\n }\n\n syncTextToProsemirror(ORIGIN_TEXT_TO_PM)\n }\n\n // Run bootstrap synchronously before installing the observer so that\n // an exception during bootstrap cannot leave a dangling observer.\n const bootstrapResult = bootstrap()\n\n sharedText.observe(textObserver)\n\n return {\n bootstrapResult,\n syncToSharedText(doc: Node): ReplaceTextResult {\n const text = serialize(doc)\n const result = replaceSharedText(sharedText, text, ORIGIN_PM_TO_TEXT, normalize)\n // Always update lastBridgedText unless truly failed (detached).\n // 'unchanged' means Y.Text already has this content — still need to\n // record it so the reverse observer doesn't trigger a redundant sync.\n if (result.ok || result.reason === 'unchanged') {\n lastBridgedText = normalize(text)\n }\n return result\n },\n isYjsSyncChange(tr: Transaction): boolean {\n // Internal meta shape from y-prosemirror's ySyncPlugin (tested against ^1.3.x).\n const meta = tr.getMeta(ySyncPluginKey)\n return (\n typeof meta === 'object' &&\n meta !== null &&\n 'isChangeOrigin' in meta &&\n (meta as Record<string, unknown>).isChangeOrigin === true\n )\n },\n dispose() {\n sharedText.unobserve(textObserver)\n },\n }\n}\n","import type { Node, Schema } from 'prosemirror-model'\nimport type { Transaction } from 'prosemirror-state'\nimport type { Serialize, Parse, Normalize, OnError } from '@pm-cm/core'\nimport type { Doc, Text as YText, XmlFragment as YXmlFragment } from 'yjs'\nimport type { ReplaceTextResult } from './bridge.js'\n\n/** Known warning codes emitted by the yjs bridge and plugins. */\nexport type WarningCode = 'bridge-already-wired' | 'sync-failed' | 'ysync-plugin-missing' | 'cursor-sync-not-installed'\n\n/** Structured warning event for non-fatal warnings. */\nexport type WarningEvent = {\n code: WarningCode\n message: string\n}\n\n/**\n * Warning handler callback for non-fatal warnings.\n *\n * Known codes:\n * - `'bridge-already-wired'` — the same bridge handle is wired to multiple plugin instances.\n * - `'sync-failed'` — `syncToSharedText` failed (e.g. Y.Text detached).\n * - `'ysync-plugin-missing'` — ySyncPlugin state is not available; cursor broadcast skipped.\n * - `'cursor-sync-not-installed'` — cursor sync plugin is not installed on the EditorView.\n */\nexport type OnWarning = (event: WarningEvent) => void\n\n/** Yjs transaction origin: text → ProseMirror direction. */\nexport const ORIGIN_TEXT_TO_PM = 'bridge:text-to-prosemirror'\n\n/** Yjs transaction origin: ProseMirror → text direction. */\nexport const ORIGIN_PM_TO_TEXT = 'bridge:prosemirror-to-text'\n\n/** Yjs transaction origin: bootstrap initialization. */\nexport const ORIGIN_INIT = 'bridge:init'\n\n/** Configuration for {@link createYjsBridge}. */\nexport type YjsBridgeConfig = {\n doc: Doc\n sharedText: YText\n sharedProseMirror: YXmlFragment\n schema: Schema\n serialize: Serialize\n parse: Parse\n normalize?: Normalize\n /** Called on non-fatal errors (e.g. parse failures). Defaults to `console.error`. */\n onError?: OnError\n}\n\n/**\n * Result of the bootstrap phase in {@link createYjsBridge}.\n * Indicates which source was used to initialize the shared types.\n */\nexport type BootstrapResult = {\n source: 'text' | 'prosemirror' | 'both-match' | 'empty' | 'initial'\n /** `true` when format conversion (parse or serialize) failed during bootstrap. The bridge is still usable but the affected shared type may be stale. */\n parseError?: boolean\n}\n\n/** Handle returned by {@link createYjsBridge}. */\nexport type YjsBridgeHandle = {\n /** Result of the synchronous bootstrap phase. */\n readonly bootstrapResult: BootstrapResult\n /** Serialize `doc` and push to `Y.Text` using minimal diff. */\n syncToSharedText(doc: Node): ReplaceTextResult\n /** Returns `true` if the transaction originated from `y-prosemirror` sync. */\n isYjsSyncChange(tr: Transaction): boolean\n /** Remove the Y.Text observer. Call when tearing down. */\n dispose(): void\n}\n","import type { Awareness } from 'y-protocols/awareness'\n\n/**\n * Create a Proxy around a Yjs {@link Awareness} that adapts the cursor sync\n * plugin's awareness field for y-prosemirror's `yCursorPlugin`.\n *\n * y-prosemirror (npm 1.3.7) hardcodes `\"cursor\"` in `createDecorations`,\n * but the cursor sync plugin writes to a separate field (default `\"pmCursor\"`)\n * to avoid conflicts with y-codemirror.next's `\"cursor\"` (Y.Text-based).\n *\n * This proxy:\n * - **`getStates()`**: remaps `cursorField` → `\"cursor\"` so yCursorPlugin\n * finds the PM cursor data under the hardcoded `\"cursor\"` key.\n * - **`getLocalState()`**: returns `cursor: null` so yCursorPlugin's\n * `updateCursorInfo` never tries to broadcast its own cursor.\n * - **`setLocalStateField(\"cursor\", …)`**: suppressed (no-op) so\n * yCursorPlugin cannot overwrite the field managed by the sync plugin.\n */\nexport function createAwarenessProxy(awareness: Awareness, cursorField = 'pmCursor'): Awareness {\n return new Proxy(awareness, {\n get(target, prop, receiver) {\n if (prop === 'getLocalState') {\n return () => {\n const state = target.getLocalState()\n // Hide \"cursor\" so yCursorPlugin's updateCursorInfo sees null\n return state ? { ...state, cursor: null } : state\n }\n }\n if (prop === 'setLocalStateField') {\n return (field: string, value: unknown) => {\n // Block yCursorPlugin's writes to \"cursor\"\n if (field === 'cursor') return\n target.setLocalStateField(field, value)\n }\n }\n if (prop === 'getStates') {\n return () => {\n const states = target.getStates()\n // Remap cursorField → \"cursor\" so yCursorPlugin reads PM cursor data\n const remapped = new Map<number, Record<string, unknown>>()\n states.forEach((state, clientId) => {\n remapped.set(clientId, {\n ...state as Record<string, unknown>,\n cursor: (state as Record<string, unknown>)[cursorField] ?? null,\n })\n })\n return remapped\n }\n }\n const value = Reflect.get(target, prop, receiver) as unknown\n return typeof value === 'function' ? (value as Function).bind(target) : value\n },\n }) as Awareness\n}\n","import type { Node, Schema } from 'prosemirror-model'\nimport type { EditorState, Plugin } from 'prosemirror-state'\nimport type { DecorationAttrs } from 'prosemirror-view'\nimport type { Awareness } from 'y-protocols/awareness'\nimport type { Serialize, SerializeWithMap } from '@pm-cm/core'\nimport { initProseMirrorDoc, yCursorPlugin, ySyncPlugin, yUndoPlugin } from 'y-prosemirror'\nimport type { AbstractType, Text as YText, UndoManager } from 'yjs'\nimport type { XmlFragment as YXmlFragment } from 'yjs'\nimport { createAwarenessProxy } from './awareness-proxy.js'\nimport { createBridgeSyncPlugin } from './bridge-sync-plugin.js'\nimport { createCursorSyncPlugin } from './cursor-sync-plugin.js'\nimport type { YjsBridgeHandle, OnWarning } from './types.js'\n\n/** Yjs ↔ ProseMirror node mapping used by `y-prosemirror`. */\nexport type ProseMirrorMapping = Map<AbstractType<unknown>, Node | Node[]>\n\n/** Options forwarded to `yCursorPlugin` from y-prosemirror. */\nexport type YCursorPluginOpts = {\n awarenessStateFilter?: (currentClientId: number, userClientId: number, user: unknown) => boolean\n cursorBuilder?: (user: unknown, clientId: number) => HTMLElement\n selectionBuilder?: (user: unknown, clientId: number) => DecorationAttrs\n getSelection?: (state: EditorState) => unknown\n}\n\n/** Options forwarded to `yUndoPlugin` from y-prosemirror. */\nexport type YUndoPluginOpts = {\n protectedNodes?: Set<string>\n trackedOrigins?: unknown[]\n undoManager?: UndoManager | null\n}\n\n/** Options for {@link createCollabPlugins}. */\nexport type CollabPluginsOptions = {\n /** Shared ProseMirror document in Yjs. */\n sharedProseMirror: YXmlFragment\n awareness: Awareness\n cursorFieldName?: string\n serialize?: Serialize | SerializeWithMap\n /** Awareness field used for CM/Y.Text cursor payloads. Default `'cursor'`. */\n cmCursorFieldName?: string\n /**\n * Enable PM↔CM cursor sync. Default `false`.\n *\n * When enabled, an {@link createAwarenessProxy | awareness proxy} is applied\n * to suppress y-prosemirror's built-in cursor management.\n */\n cursorSync?: boolean\n /**\n * The shared `Y.Text` instance. When provided, the cursor sync plugin also\n * broadcasts CM-format cursor positions so remote `yCollab` instances render them.\n */\n sharedText?: YText\n /**\n * When provided, a bridge sync plugin is inserted before the cursor sync plugin\n * to ensure Y.Text is synced before cursor positions are computed. This guarantees\n * that serialize-based offsets match Y.Text indices.\n */\n bridge?: YjsBridgeHandle\n /** Extra options forwarded to `yCursorPlugin`. */\n yCursorPluginOpts?: YCursorPluginOpts\n /** Extra options forwarded to `yUndoPlugin`. */\n yUndoPluginOpts?: YUndoPluginOpts\n /** Called for non-fatal warnings. Propagated to child plugins. Default `console.warn`. */\n onWarning?: OnWarning\n}\n\n/**\n * Bundle `ySyncPlugin`, `yCursorPlugin`, `yUndoPlugin` from y-prosemirror,\n * plus an optional PM↔CM cursor sync plugin.\n *\n * @throws If `cursorSync: true` but `serialize` is not provided.\n */\nexport function createCollabPlugins(\n schema: Schema,\n options: CollabPluginsOptions,\n): { plugins: Plugin[]; doc: Node; mapping: ProseMirrorMapping } {\n const cursorFieldName = options.cursorFieldName ?? 'pmCursor'\n const enableCursorSync = options.cursorSync ?? false\n const { sharedProseMirror } = options\n\n if (enableCursorSync && !options.serialize) {\n throw new Error('createCollabPlugins: cursorSync requires serialize to be provided')\n }\n const { doc, mapping: rawMapping } = initProseMirrorDoc(sharedProseMirror, schema)\n const mapping = rawMapping as ProseMirrorMapping\n const pmAwareness = enableCursorSync\n ? createAwarenessProxy(options.awareness, cursorFieldName)\n : options.awareness\n\n const plugins: Plugin[] = [\n ySyncPlugin(sharedProseMirror, { mapping: rawMapping }),\n yCursorPlugin(pmAwareness, options.yCursorPluginOpts ?? {}),\n yUndoPlugin(options.yUndoPluginOpts),\n ]\n\n // Bridge sync plugin must run before cursor sync plugin so that\n // Y.Text is updated before cursor positions are computed.\n if (options.bridge) {\n plugins.push(createBridgeSyncPlugin(options.bridge, { onWarning: options.onWarning }))\n }\n\n if (enableCursorSync && options.serialize) {\n plugins.push(\n createCursorSyncPlugin({\n awareness: options.awareness,\n serialize: options.serialize,\n cursorFieldName,\n cmCursorFieldName: options.cmCursorFieldName,\n sharedText: options.sharedText,\n onWarning: options.onWarning,\n }),\n )\n }\n\n return { plugins, doc, mapping }\n}\n","import { Plugin, PluginKey } from 'prosemirror-state'\nimport type { EditorView } from 'prosemirror-view'\nimport type { YjsBridgeHandle, OnWarning } from './types.js'\n\ntype BridgeSyncState = { needsSync: boolean }\n\ntype BridgeSyncFailure = { ok: false; reason: 'detached' }\n\n/** Options for {@link createBridgeSyncPlugin}. */\nexport type BridgeSyncPluginOptions = {\n /** Called when `syncToSharedText` fails (excludes `reason: 'unchanged'`). */\n onSyncFailure?: (result: BridgeSyncFailure, view: EditorView) => void\n /** Called for non-fatal warnings. Default `console.warn`. */\n onWarning?: OnWarning\n}\n\n/** ProseMirror plugin key for {@link createBridgeSyncPlugin}. Use to read the plugin state. */\nexport const bridgeSyncPluginKey = new PluginKey<BridgeSyncState>('pm-cm-bridge-sync')\n\nconst wiredBridges = new WeakSet<YjsBridgeHandle>()\n\nconst defaultOnWarning: OnWarning = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`)\n\n/**\n * ProseMirror plugin that automatically syncs PM doc changes to Y.Text\n * via the bridge handle. Skips Yjs-originated changes to avoid loops.\n *\n * A warning is logged if the same bridge handle is wired more than once.\n * The guard is cleaned up when the plugin is destroyed.\n */\nexport function createBridgeSyncPlugin(\n bridge: YjsBridgeHandle,\n options: BridgeSyncPluginOptions = {},\n): Plugin {\n const warn = options.onWarning ?? defaultOnWarning\n if (wiredBridges.has(bridge)) {\n warn({ code: 'bridge-already-wired', message: 'this bridge is already wired to another plugin instance' })\n }\n wiredBridges.add(bridge)\n\n return new Plugin<BridgeSyncState>({\n key: bridgeSyncPluginKey,\n\n state: {\n init(): BridgeSyncState {\n return { needsSync: false }\n },\n apply(tr, _prev): BridgeSyncState {\n if (!tr.docChanged) return { needsSync: false }\n if (bridge.isYjsSyncChange(tr)) return { needsSync: false }\n return { needsSync: true }\n },\n },\n\n view() {\n return {\n update(view) {\n const state = bridgeSyncPluginKey.getState(view.state)\n if (state?.needsSync) {\n const result = bridge.syncToSharedText(view.state.doc)\n if (!result.ok) {\n if (result.reason === 'detached') {\n options.onSyncFailure?.(result, view)\n warn({ code: 'sync-failed', message: `bridge sync failed: ${result.reason}` })\n }\n }\n }\n },\n destroy() {\n wiredBridges.delete(bridge)\n },\n }\n },\n })\n}\n","import { Plugin, PluginKey } from 'prosemirror-state'\nimport type { Node } from 'prosemirror-model'\nimport type { EditorView } from 'prosemirror-view'\nimport type { Awareness } from 'y-protocols/awareness'\nimport { absolutePositionToRelativePosition, ySyncPluginKey } from 'y-prosemirror'\nimport { createRelativePositionFromTypeIndex } from 'yjs'\nimport type { Text as YText, XmlFragment as YXmlFragment } from 'yjs'\nimport type { Serialize, SerializeWithMap, CursorMap } from '@pm-cm/core'\nimport { buildCursorMap, cursorMapLookup, reverseCursorMapLookup } from '@pm-cm/core'\nimport type { OnWarning } from './types.js'\n\n/** Plugin state for the cursor sync plugin. Read via {@link cursorSyncPluginKey}. */\nexport type CursorSyncState = {\n /** Pending CodeMirror cursor to broadcast. Set by {@link syncCmCursor}. */\n pendingCm: { anchor: number; head: number } | null\n /** Text offset mapped from the current PM selection anchor. `null` when no mapping is available. */\n mappedTextOffset: number | null\n}\n\n/** ProseMirror plugin key for {@link createCursorSyncPlugin}. Use to read the plugin state. */\nexport const cursorSyncPluginKey = new PluginKey<CursorSyncState>('pm-cm-cursor-sync')\n\n/**\n * Internal shape of `ySyncPluginKey` state from y-prosemirror.\n * Not exported by upstream — kept here for explicit tracking.\n * Tested against y-prosemirror ^1.3.x.\n */\ntype YSyncPluginState = { type: YXmlFragment; binding: { mapping: Map<unknown, unknown> } }\n\nfunction getYSyncState(view: EditorView): YSyncPluginState | null {\n const raw = ySyncPluginKey.getState(view.state) as Record<string, unknown> | undefined\n if (!raw) return null\n if (\n typeof raw === 'object' &&\n 'type' in raw && raw.type &&\n 'binding' in raw && raw.binding &&\n typeof raw.binding === 'object' &&\n 'mapping' in (raw.binding as Record<string, unknown>) &&\n (raw.binding as Record<string, unknown>).mapping instanceof Map\n ) {\n return raw as unknown as YSyncPluginState\n }\n return null\n}\n\nfunction toRelativePosition(\n view: EditorView,\n pmPos: number,\n): unknown | null {\n const ySyncState = getYSyncState(view)\n if (!ySyncState) return null\n\n return absolutePositionToRelativePosition(\n pmPos,\n ySyncState.type,\n ySyncState.binding.mapping as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- y-prosemirror internal mapping type\n )\n}\n\n/** Returns `false` when ySyncPlugin state is unavailable (plugin not installed). */\nfunction broadcastPmCursor(\n awareness: Awareness,\n cursorFieldName: string,\n view: EditorView,\n pmAnchor: number,\n pmHead: number,\n): boolean {\n const relAnchor = toRelativePosition(view, pmAnchor)\n const relHead = toRelativePosition(view, pmHead)\n if (relAnchor === null || relHead === null) return false\n\n awareness.setLocalStateField(cursorFieldName, { anchor: relAnchor, head: relHead })\n return true\n}\n\nfunction broadcastTextCursor(\n awareness: Awareness,\n cmCursorFieldName: string,\n sharedText: YText,\n textAnchor: number,\n textHead: number,\n): void {\n const len = sharedText.length\n const clamp = (v: number) => Math.max(0, Math.min(v, len))\n const relAnchor = createRelativePositionFromTypeIndex(sharedText, clamp(textAnchor))\n const relHead = createRelativePositionFromTypeIndex(sharedText, clamp(textHead))\n awareness.setLocalStateField(cmCursorFieldName, { anchor: relAnchor, head: relHead })\n}\n\nconst defaultOnWarning: OnWarning = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`)\n\n/** Options for {@link createCursorSyncPlugin}. */\nexport type CursorSyncPluginOptions = {\n awareness: Awareness\n serialize: Serialize | SerializeWithMap\n cursorFieldName?: string\n /** Awareness field used for CM/Y.Text cursor payloads. Default `'cursor'`. */\n cmCursorFieldName?: string\n /**\n * When provided, the plugin also broadcasts CM-format cursor positions\n * (Y.Text relative positions) to the awareness field specified by\n * `cmCursorFieldName`, so that remote `yCollab` instances can render the cursor.\n */\n sharedText?: YText\n /** Called for non-fatal warnings. Default `console.warn`. */\n onWarning?: OnWarning\n}\n\n/**\n * ProseMirror plugin that synchronizes cursor positions between PM and CM via Yjs awareness.\n *\n * - PM → awareness: automatically broadcasts when the PM view is focused and selection changes.\n * - CM → awareness: triggered by dispatching {@link syncCmCursor}.\n */\nexport function createCursorSyncPlugin(options: CursorSyncPluginOptions): Plugin {\n const { awareness, serialize, sharedText } = options\n const warn = options.onWarning ?? defaultOnWarning\n const cursorFieldName = options.cursorFieldName ?? 'pmCursor'\n const cmCursorFieldName = options.cmCursorFieldName ?? 'cursor'\n\n let warnedSyncPluginMissing = false\n\n // Cached cursor map (serialize-based) — rebuilt only when doc changes\n let cachedMap: CursorMap | null = null\n let cachedMapDoc: Node | null = null\n\n function getOrBuildMap(doc: Node): CursorMap {\n if (cachedMapDoc !== doc || !cachedMap) {\n cachedMap = buildCursorMap(doc, serialize)\n cachedMapDoc = doc\n }\n return cachedMap\n }\n\n return new Plugin<CursorSyncState>({\n key: cursorSyncPluginKey,\n\n state: {\n init(): CursorSyncState {\n return { pendingCm: null, mappedTextOffset: null }\n },\n apply(tr, prev, _oldState, newState): CursorSyncState {\n const cmMeta = tr.getMeta(cursorSyncPluginKey) as\n | { anchor: number; head: number }\n | undefined\n if (cmMeta) {\n return { pendingCm: cmMeta, mappedTextOffset: prev.mappedTextOffset }\n }\n\n // Compute PM → text offset when selection or doc changes\n let mappedTextOffset = prev.mappedTextOffset\n if (tr.selectionSet || tr.docChanged) {\n const map = getOrBuildMap(newState.doc)\n mappedTextOffset = cursorMapLookup(map, newState.selection.anchor)\n }\n\n return {\n pendingCm: prev.pendingCm !== null ? null : prev.pendingCm,\n mappedTextOffset,\n }\n },\n },\n\n view() {\n return {\n update(view, prevState) {\n const pluginState = cursorSyncPluginKey.getState(view.state)\n const prevPluginState = cursorSyncPluginKey.getState(prevState)\n\n // CM → awareness: broadcast when pendingCm is newly set\n if (\n pluginState?.pendingCm != null &&\n pluginState.pendingCm !== prevPluginState?.pendingCm\n ) {\n const map = getOrBuildMap(view.state.doc)\n const pmAnchor = reverseCursorMapLookup(map, pluginState.pendingCm.anchor)\n const pmHead = reverseCursorMapLookup(map, pluginState.pendingCm.head)\n if (pmAnchor !== null && pmHead !== null) {\n const ok = broadcastPmCursor(awareness, cursorFieldName, view, pmAnchor, pmHead)\n if (!ok && !warnedSyncPluginMissing) {\n warnedSyncPluginMissing = true\n warn({ code: 'ysync-plugin-missing', message: 'ySyncPlugin state not available — cursor broadcast skipped' })\n }\n }\n // Also broadcast CM-format cursor so remote yCollab can render it\n if (sharedText) {\n broadcastTextCursor(\n awareness,\n cmCursorFieldName,\n sharedText,\n pluginState.pendingCm.anchor,\n pluginState.pendingCm.head,\n )\n }\n return\n }\n\n // PM → awareness: auto-broadcast on selection/doc change when focused\n if (\n view.hasFocus() &&\n (view.state.selection !== prevState.selection ||\n view.state.doc !== prevState.doc)\n ) {\n const { anchor, head } = view.state.selection\n const ok = broadcastPmCursor(awareness, cursorFieldName, view, anchor, head)\n if (!ok && !warnedSyncPluginMissing) {\n warnedSyncPluginMissing = true\n warn({ code: 'ysync-plugin-missing', message: 'ySyncPlugin state not available — cursor broadcast skipped' })\n }\n // Also broadcast CM-format cursor so remote yCollab can render it.\n // When bridgeSyncPlugin runs before this plugin, Y.Text is already\n // synced so serialize-based offsets match Y.Text indices.\n if (sharedText) {\n const map = getOrBuildMap(view.state.doc)\n const textAnchor = cursorMapLookup(map, anchor)\n const textHead = cursorMapLookup(map, head)\n if (textAnchor !== null && textHead !== null) {\n broadcastTextCursor(awareness, cmCursorFieldName, sharedText, textAnchor, textHead)\n }\n }\n }\n },\n }\n },\n })\n}\n\n/**\n * Dispatch a CodeMirror cursor offset (or range) to the cursor sync plugin.\n * The plugin will convert it to a ProseMirror position and broadcast via awareness.\n *\n * @param view - The ProseMirror EditorView that has the cursor sync plugin installed.\n * @param anchor - CodeMirror text offset for the anchor.\n * @param head - CodeMirror text offset for the head (defaults to `anchor` for a collapsed cursor).\n * @param onWarning - Optional warning callback. Default `console.warn`.\n */\nexport function syncCmCursor(view: EditorView, anchor: number, head?: number, onWarning?: OnWarning): void {\n if (!cursorSyncPluginKey.getState(view.state)) {\n (onWarning ?? defaultOnWarning)({ code: 'cursor-sync-not-installed', message: 'cursor sync plugin is not installed on this EditorView' })\n return\n }\n const sanitize = (v: number) => Math.max(0, Math.floor(v))\n view.dispatch(\n view.state.tr.setMeta(cursorSyncPluginKey, {\n anchor: sanitize(anchor),\n head: sanitize(head ?? anchor),\n }),\n )\n}\n","export { createYjsBridge, replaceSharedText, replaceSharedProseMirror } from './bridge.js'\nexport type { YjsBridgeOptions, ReplaceResult, ReplaceTextResult, ReplaceProseMirrorResult } from './bridge.js'\nexport { createAwarenessProxy } from './awareness-proxy.js'\nexport { createCollabPlugins } from './collab-plugins.js'\nexport type { CollabPluginsOptions, ProseMirrorMapping, YCursorPluginOpts, YUndoPluginOpts } from './collab-plugins.js'\nexport {\n ORIGIN_TEXT_TO_PM,\n ORIGIN_PM_TO_TEXT,\n ORIGIN_INIT,\n} from './types.js'\nexport type {\n BootstrapResult,\n YjsBridgeConfig,\n YjsBridgeHandle,\n WarningCode,\n WarningEvent,\n OnWarning,\n} from './types.js'\n\n// Cursor mapping re-exported from @pm-cm/core\nexport { buildCursorMap, cursorMapLookup, reverseCursorMapLookup } from '@pm-cm/core'\nexport type { TextSegment, CursorMap, SerializeWithMap, CursorMapWriter, Matcher, MatchResult, MatchRun } from '@pm-cm/core'\n\n// Bridge sync plugin (auto PM→Y.Text wiring)\nexport { createBridgeSyncPlugin, bridgeSyncPluginKey } from './bridge-sync-plugin.js'\nexport type { BridgeSyncPluginOptions } from './bridge-sync-plugin.js'\n\n// Cursor sync plugin\nexport { createCursorSyncPlugin, cursorSyncPluginKey, syncCmCursor } from './cursor-sync-plugin.js'\nexport type { CursorSyncState, CursorSyncPluginOptions } from './cursor-sync-plugin.js'\n\n// Re-export types from @pm-cm/core\nexport { createCursorMapWriter, wrapSerialize } from '@pm-cm/core'\nexport type { Serialize, Parse, Normalize, OnError, ErrorCode, ErrorEvent } from '@pm-cm/core'\n"],"mappings":"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","names":["doc","value","Plugin","PluginKey","ySyncPluginKey","defaultOnWarning","buildCursorMap","cursorMapLookup","reverseCursorMapLookup"]}
|
|
1
|
+
{"version":3,"sources":["../src/bridge.ts","../src/types.ts","../src/awareness-proxy.ts","../src/collab-plugins.ts","../src/bridge-sync-plugin.ts","../src/cursor-sync-plugin.ts","../src/index.ts"],"sourcesContent":["import type { Node } from 'prosemirror-model'\nimport type { Transaction } from 'prosemirror-state'\nimport { prosemirrorToYXmlFragment, yXmlFragmentToProseMirrorRootNode, ySyncPluginKey } from 'y-prosemirror'\nimport type { Doc, Text as YText, XmlFragment as YXmlFragment } from 'yjs'\nimport type { Normalize, OnError } from '@pm-cm/core'\nimport type { BootstrapResult, YjsBridgeConfig, YjsBridgeHandle } from './types.js'\nimport { ORIGIN_INIT, ORIGIN_TEXT_TO_PM, ORIGIN_PM_TO_TEXT } from './types.js'\n\nconst defaultNormalize: Normalize = (s) => s.replace(/\\r\\n?/g, '\\n')\nconst defaultOnError: OnError = (event) => console.error(`[bridge] ${event.code}: ${event.message}`, event.cause)\n\n/** Result of {@link replaceSharedText}. */\nexport type ReplaceTextResult =\n | { ok: true }\n | { ok: false; reason: 'unchanged' }\n | { ok: false; reason: 'detached' }\n | { ok: false; reason: 'serialize-error' }\n\n/** Result of {@link replaceSharedProseMirror}. */\nexport type ReplaceProseMirrorResult =\n | { ok: true }\n | { ok: false; reason: 'parse-error' }\n | { ok: false; reason: 'detached' }\n\n/**\n * Union of all replace-result types. Kept for backward compatibility.\n * Prefer the narrower {@link ReplaceTextResult} / {@link ReplaceProseMirrorResult}.\n */\nexport type ReplaceResult = ReplaceTextResult | ReplaceProseMirrorResult\n\n/**\n * Replace `Y.Text` content using a minimal diff (common prefix/suffix trimming).\n * Returns a {@link ReplaceResult} indicating success or failure reason.\n */\nexport function replaceSharedText(\n sharedText: YText,\n next: string,\n origin: unknown,\n normalize: Normalize = defaultNormalize,\n): ReplaceTextResult {\n if (!sharedText.doc) {\n return { ok: false, reason: 'detached' }\n }\n\n const normalized = normalize(next)\n const current = sharedText.toString()\n if (current === normalized) {\n return { ok: false, reason: 'unchanged' }\n }\n\n // Minimal diff: find common prefix and suffix, replace only the changed middle.\n let start = 0\n const minLen = Math.min(current.length, normalized.length)\n while (start < minLen && current.charCodeAt(start) === normalized.charCodeAt(start)) {\n start++\n }\n\n let endCurrent = current.length\n let endNext = normalized.length\n while (endCurrent > start && endNext > start && current.charCodeAt(endCurrent - 1) === normalized.charCodeAt(endNext - 1)) {\n endCurrent--\n endNext--\n }\n\n sharedText.doc.transact(() => {\n const deleteCount = endCurrent - start\n if (deleteCount > 0) {\n sharedText.delete(start, deleteCount)\n }\n const insertStr = normalized.slice(start, endNext)\n if (insertStr.length > 0) {\n sharedText.insert(start, insertStr)\n }\n }, origin)\n\n return { ok: true }\n}\n\n/**\n * Replace `Y.XmlFragment` by parsing serialized text into a ProseMirror document.\n * Returns a {@link ReplaceResult} indicating success or failure reason.\n */\nexport function replaceSharedProseMirror(\n doc: Doc,\n fragment: YXmlFragment,\n text: string,\n origin: unknown,\n config: Pick<YjsBridgeConfig, 'schema' | 'parse' | 'normalize' | 'onError'>,\n): ReplaceProseMirrorResult {\n if (!fragment.doc) {\n return { ok: false, reason: 'detached' }\n }\n if (fragment.doc !== doc) {\n throw new Error('fragment belongs to a different Y.Doc than the provided doc')\n }\n\n const normalize = config.normalize ?? defaultNormalize\n const onError = config.onError ?? defaultOnError\n let nextDoc: Node\n try {\n nextDoc = config.parse(normalize(text), config.schema)\n } catch (error) {\n onError({ code: 'parse-error', message: 'failed to parse text into ProseMirror document', cause: error })\n return { ok: false, reason: 'parse-error' }\n }\n\n doc.transact(() => {\n prosemirrorToYXmlFragment(nextDoc, fragment)\n }, origin)\n return { ok: true }\n}\n\n/** Options for {@link createYjsBridge}. */\nexport type YjsBridgeOptions = {\n initialText?: string\n /** Which side wins when both sharedText and sharedProseMirror exist and differ. Default `'text'`. */\n prefer?: 'text' | 'prosemirror'\n}\n\n/**\n * Create a collaborative bridge that keeps `Y.Text` and `Y.XmlFragment` in sync.\n *\n * Runs a synchronous bootstrap to reconcile existing state, then installs a\n * `Y.Text` observer for the text → ProseMirror direction.\n *\n * @throws If `sharedText` or `sharedProseMirror` belong to a different `Y.Doc`.\n */\nexport function createYjsBridge(\n config: YjsBridgeConfig,\n options?: YjsBridgeOptions,\n): YjsBridgeHandle {\n const {\n doc,\n sharedText,\n sharedProseMirror,\n schema,\n serialize,\n parse,\n } = config\n const normalize = config.normalize ?? defaultNormalize\n const onError = config.onError ?? defaultOnError\n\n if (!sharedText.doc) {\n throw new Error('sharedText is not attached to any Y.Doc')\n }\n if (sharedText.doc !== doc) {\n throw new Error('sharedText belongs to a different Y.Doc than the provided doc')\n }\n if (!sharedProseMirror.doc) {\n throw new Error('sharedProseMirror is not attached to any Y.Doc')\n }\n if (sharedProseMirror.doc !== doc) {\n throw new Error('sharedProseMirror belongs to a different Y.Doc than the provided doc')\n }\n\n let lastBridgedText: string | null = null\n\n /** Returns `true` if the parse succeeded. */\n const syncTextToProsemirror = (origin: unknown): boolean => {\n const text = normalize(sharedText.toString())\n if (lastBridgedText === text) {\n return true\n }\n\n const result = replaceSharedProseMirror(doc, sharedProseMirror, text, origin, {\n schema,\n parse,\n normalize,\n onError,\n })\n if (result.ok) {\n lastBridgedText = text\n }\n return result.ok\n }\n\n const sharedProseMirrorToText = (fragment: YXmlFragment): string | null => {\n try {\n const pmDoc = yXmlFragmentToProseMirrorRootNode(fragment, schema)\n return normalize(serialize(pmDoc))\n } catch (error) {\n onError({ code: 'serialize-error', message: 'failed to convert ProseMirror fragment to text', cause: error })\n return null\n }\n }\n\n // Bootstrap\n const bootstrap = (): BootstrapResult => {\n const text = normalize(sharedText.toString())\n const hasText = text.length > 0\n const hasProsemirror = sharedProseMirror.length > 0\n\n if (!hasText && !hasProsemirror) {\n const initial = options?.initialText ?? ''\n if (initial.length > 0) {\n // Set Y.XmlFragment first, then derive Y.Text from serialize(parse(initial))\n // to ensure both shared types are in the same canonical form.\n const initResult = replaceSharedProseMirror(doc, sharedProseMirror, initial, ORIGIN_INIT, {\n schema,\n parse,\n normalize,\n onError,\n })\n if (!initResult.ok) {\n return { source: 'initial', parseError: true }\n }\n const pmDoc = yXmlFragmentToProseMirrorRootNode(sharedProseMirror, schema)\n let canonicalText: string\n try {\n canonicalText = serialize(pmDoc)\n } catch (error) {\n onError({ code: 'serialize-error', message: 'failed to serialize ProseMirror document during bootstrap', cause: error })\n return { source: 'initial', parseError: true }\n }\n replaceSharedText(sharedText, canonicalText, ORIGIN_INIT, normalize)\n lastBridgedText = normalize(canonicalText)\n return { source: 'initial' }\n }\n return { source: 'empty' }\n }\n\n if (hasText && !hasProsemirror) {\n const ok = syncTextToProsemirror(ORIGIN_INIT)\n return { source: 'text', ...(!ok && { parseError: true }) }\n }\n\n if (!hasText && hasProsemirror) {\n const textFromProsemirror = sharedProseMirrorToText(sharedProseMirror)\n if (textFromProsemirror !== null) {\n replaceSharedText(sharedText, textFromProsemirror, ORIGIN_INIT, normalize)\n lastBridgedText = normalize(textFromProsemirror)\n return { source: 'prosemirror' }\n }\n return { source: 'prosemirror', parseError: true }\n }\n\n const prosemirrorText = sharedProseMirrorToText(sharedProseMirror)\n if (prosemirrorText === null) {\n const fallbackText = hasText ? text : (options?.initialText ?? '')\n let parseError = false\n if (fallbackText.length > 0) {\n replaceSharedText(sharedText, fallbackText, ORIGIN_INIT, normalize)\n const fallbackResult = replaceSharedProseMirror(doc, sharedProseMirror, fallbackText, ORIGIN_INIT, {\n schema,\n parse,\n normalize,\n onError,\n })\n if (!fallbackResult.ok) parseError = true\n }\n return { source: 'text', ...(parseError && { parseError: true }) }\n }\n\n if (prosemirrorText !== text) {\n const prefer = options?.prefer ?? 'text'\n if (prefer === 'prosemirror') {\n replaceSharedText(sharedText, prosemirrorText, ORIGIN_INIT, normalize)\n lastBridgedText = normalize(prosemirrorText)\n return { source: 'prosemirror' }\n } else {\n const ok = syncTextToProsemirror(ORIGIN_INIT)\n return { source: 'text', ...(!ok && { parseError: true }) }\n }\n } else {\n lastBridgedText = text\n return { source: 'both-match' }\n }\n }\n\n const textObserver = (\n _: unknown,\n transaction: { origin: unknown },\n ) => {\n if (transaction.origin === ORIGIN_PM_TO_TEXT || transaction.origin === ORIGIN_INIT) {\n return\n }\n\n syncTextToProsemirror(ORIGIN_TEXT_TO_PM)\n }\n\n // Run bootstrap synchronously before installing the observer so that\n // an exception during bootstrap cannot leave a dangling observer.\n const bootstrapResult = bootstrap()\n\n sharedText.observe(textObserver)\n\n return {\n bootstrapResult,\n syncToSharedText(doc: Node): ReplaceTextResult {\n let text: string\n try {\n text = serialize(doc)\n } catch (error) {\n onError({ code: 'serialize-error', message: 'failed to serialize ProseMirror document', cause: error })\n return { ok: false, reason: 'serialize-error' }\n }\n const result = replaceSharedText(sharedText, text, ORIGIN_PM_TO_TEXT, normalize)\n // Always update lastBridgedText unless truly failed (detached).\n // 'unchanged' means Y.Text already has this content — still need to\n // record it so the reverse observer doesn't trigger a redundant sync.\n if (result.ok || result.reason === 'unchanged') {\n lastBridgedText = normalize(text)\n }\n return result\n },\n isYjsSyncChange(tr: Transaction): boolean {\n // Internal meta shape from y-prosemirror's ySyncPlugin (tested against ^1.3.x).\n const meta = tr.getMeta(ySyncPluginKey)\n return (\n typeof meta === 'object' &&\n meta !== null &&\n 'isChangeOrigin' in meta &&\n (meta as Record<string, unknown>).isChangeOrigin === true\n )\n },\n dispose() {\n sharedText.unobserve(textObserver)\n },\n }\n}\n","import type { Node, Schema } from 'prosemirror-model'\nimport type { Transaction } from 'prosemirror-state'\nimport type { Serialize, Parse, Normalize, OnError } from '@pm-cm/core'\nimport type { Doc, Text as YText, XmlFragment as YXmlFragment } from 'yjs'\nimport type { ReplaceTextResult } from './bridge.js'\n\n/** Known warning codes emitted by the yjs bridge and plugins. */\nexport type WarningCode = 'bridge-already-wired' | 'sync-failed' | 'ysync-plugin-missing' | 'cursor-sync-not-installed' | 'cursor-map-error'\n\n/** Structured warning event for non-fatal warnings. */\nexport type WarningEvent = {\n code: WarningCode\n message: string\n}\n\n/**\n * Warning handler callback for non-fatal warnings.\n *\n * Known codes:\n * - `'bridge-already-wired'` — the same bridge handle is wired to multiple plugin instances.\n * - `'sync-failed'` — `syncToSharedText` failed (e.g. Y.Text detached).\n * - `'ysync-plugin-missing'` — ySyncPlugin state is not available; cursor broadcast skipped.\n * - `'cursor-sync-not-installed'` — cursor sync plugin is not installed on the EditorView.\n * - `'cursor-map-error'` — failed to build cursor map (serialize threw); cursor sync skipped.\n */\nexport type OnWarning = (event: WarningEvent) => void\n\n/** Yjs transaction origin: text → ProseMirror direction. */\nexport const ORIGIN_TEXT_TO_PM = 'bridge:text-to-prosemirror'\n\n/** Yjs transaction origin: ProseMirror → text direction. */\nexport const ORIGIN_PM_TO_TEXT = 'bridge:prosemirror-to-text'\n\n/** Yjs transaction origin: bootstrap initialization. */\nexport const ORIGIN_INIT = 'bridge:init'\n\n/** Configuration for {@link createYjsBridge}. */\nexport type YjsBridgeConfig = {\n doc: Doc\n sharedText: YText\n sharedProseMirror: YXmlFragment\n schema: Schema\n serialize: Serialize\n parse: Parse\n normalize?: Normalize\n /** Called on non-fatal errors (e.g. parse failures). Defaults to `console.error`. */\n onError?: OnError\n}\n\n/**\n * Result of the bootstrap phase in {@link createYjsBridge}.\n * Indicates which source was used to initialize the shared types.\n */\nexport type BootstrapResult = {\n source: 'text' | 'prosemirror' | 'both-match' | 'empty' | 'initial'\n /** `true` when format conversion (parse or serialize) failed during bootstrap. The bridge is still usable but the affected shared type may be stale. */\n parseError?: boolean\n}\n\n/** Handle returned by {@link createYjsBridge}. */\nexport type YjsBridgeHandle = {\n /** Result of the synchronous bootstrap phase. */\n readonly bootstrapResult: BootstrapResult\n /** Serialize `doc` and push to `Y.Text` using minimal diff. */\n syncToSharedText(doc: Node): ReplaceTextResult\n /** Returns `true` if the transaction originated from `y-prosemirror` sync. */\n isYjsSyncChange(tr: Transaction): boolean\n /** Remove the Y.Text observer. Call when tearing down. */\n dispose(): void\n}\n","import type { Awareness } from 'y-protocols/awareness'\n\n/**\n * Create a Proxy around a Yjs {@link Awareness} that adapts the cursor sync\n * plugin's awareness field for y-prosemirror's `yCursorPlugin`.\n *\n * y-prosemirror (npm 1.3.7) hardcodes `\"cursor\"` in `createDecorations`,\n * but the cursor sync plugin writes to a separate field (default `\"pmCursor\"`)\n * to avoid conflicts with y-codemirror.next's `\"cursor\"` (Y.Text-based).\n *\n * This proxy:\n * - **`getStates()`**: remaps `cursorField` → `\"cursor\"` so yCursorPlugin\n * finds the PM cursor data under the hardcoded `\"cursor\"` key.\n * - **`getLocalState()`**: returns `cursor: null` so yCursorPlugin's\n * `updateCursorInfo` never tries to broadcast its own cursor.\n * - **`setLocalStateField(\"cursor\", …)`**: suppressed (no-op) so\n * yCursorPlugin cannot overwrite the field managed by the sync plugin.\n */\nexport function createAwarenessProxy(awareness: Awareness, cursorField = 'pmCursor'): Awareness {\n return new Proxy(awareness, {\n get(target, prop, receiver) {\n if (prop === 'getLocalState') {\n return () => {\n const state = target.getLocalState()\n // Hide \"cursor\" so yCursorPlugin's updateCursorInfo sees null\n return state ? { ...state, cursor: null } : state\n }\n }\n if (prop === 'setLocalStateField') {\n return (field: string, value: unknown) => {\n // Block yCursorPlugin's writes to \"cursor\"\n if (field === 'cursor') return\n target.setLocalStateField(field, value)\n }\n }\n if (prop === 'getStates') {\n return () => {\n const states = target.getStates()\n // Remap cursorField → \"cursor\" so yCursorPlugin reads PM cursor data.\n // Only override when the client actually has cursorField — clients\n // without cursor sync (e.g. using yCursorPlugin directly) keep their\n // original \"cursor\" intact.\n const remapped = new Map<number, Record<string, unknown>>()\n states.forEach((state, clientId) => {\n const s = state as Record<string, unknown>\n if (cursorField in s) {\n remapped.set(clientId, { ...s, cursor: s[cursorField] ?? null })\n } else {\n remapped.set(clientId, s)\n }\n })\n return remapped\n }\n }\n const value = Reflect.get(target, prop, receiver) as unknown\n return typeof value === 'function' ? (value as Function).bind(target) : value\n },\n }) as Awareness\n}\n","import type { Node, Schema } from 'prosemirror-model'\nimport type { EditorState, Plugin } from 'prosemirror-state'\nimport type { DecorationAttrs } from 'prosemirror-view'\nimport type { Awareness } from 'y-protocols/awareness'\nimport type { Serialize, SerializeWithMap } from '@pm-cm/core'\nimport { initProseMirrorDoc, yCursorPlugin, ySyncPlugin, yUndoPlugin } from 'y-prosemirror'\nimport type { AbstractType, Text as YText, UndoManager } from 'yjs'\nimport type { XmlFragment as YXmlFragment } from 'yjs'\nimport { createAwarenessProxy } from './awareness-proxy.js'\nimport { createBridgeSyncPlugin } from './bridge-sync-plugin.js'\nimport { createCursorSyncPlugin } from './cursor-sync-plugin.js'\nimport type { YjsBridgeHandle, OnWarning } from './types.js'\n\n/** Yjs ↔ ProseMirror node mapping used by `y-prosemirror`. */\nexport type ProseMirrorMapping = Map<AbstractType<unknown>, Node | Node[]>\n\n/** Options forwarded to `yCursorPlugin` from y-prosemirror. */\nexport type YCursorPluginOpts = {\n awarenessStateFilter?: (currentClientId: number, userClientId: number, user: unknown) => boolean\n cursorBuilder?: (user: unknown, clientId: number) => HTMLElement\n selectionBuilder?: (user: unknown, clientId: number) => DecorationAttrs\n getSelection?: (state: EditorState) => unknown\n}\n\n/** Options forwarded to `yUndoPlugin` from y-prosemirror. */\nexport type YUndoPluginOpts = {\n protectedNodes?: Set<string>\n trackedOrigins?: unknown[]\n undoManager?: UndoManager | null\n}\n\n/** Options for {@link createCollabPlugins}. */\nexport type CollabPluginsOptions = {\n /** Shared ProseMirror document in Yjs. */\n sharedProseMirror: YXmlFragment\n awareness: Awareness\n cursorFieldName?: string\n serialize?: Serialize | SerializeWithMap\n /** Awareness field used for CM/Y.Text cursor payloads. Default `'cursor'`. */\n cmCursorFieldName?: string\n /**\n * Enable PM↔CM cursor sync. Default `false`.\n *\n * When enabled, an {@link createAwarenessProxy | awareness proxy} is applied\n * to suppress y-prosemirror's built-in cursor management.\n */\n cursorSync?: boolean\n /**\n * The shared `Y.Text` instance. When provided, the cursor sync plugin also\n * broadcasts CM-format cursor positions so remote `yCollab` instances render them.\n */\n sharedText?: YText\n /**\n * When provided, a bridge sync plugin is inserted before the cursor sync plugin\n * to ensure Y.Text is synced before cursor positions are computed. This guarantees\n * that serialize-based offsets match Y.Text indices.\n */\n bridge?: YjsBridgeHandle\n /** Extra options forwarded to `yCursorPlugin`. */\n yCursorPluginOpts?: YCursorPluginOpts\n /** Extra options forwarded to `yUndoPlugin`. */\n yUndoPluginOpts?: YUndoPluginOpts\n /** Called for non-fatal warnings. Propagated to child plugins. Default `console.warn`. */\n onWarning?: OnWarning\n}\n\n/**\n * Bundle `ySyncPlugin`, `yCursorPlugin`, `yUndoPlugin` from y-prosemirror,\n * plus an optional PM↔CM cursor sync plugin.\n *\n * @throws If `cursorSync: true` but `serialize` is not provided.\n */\nexport function createCollabPlugins(\n schema: Schema,\n options: CollabPluginsOptions,\n): { plugins: Plugin[]; doc: Node; mapping: ProseMirrorMapping } {\n const cursorFieldName = options.cursorFieldName ?? 'pmCursor'\n const enableCursorSync = options.cursorSync ?? false\n const { sharedProseMirror } = options\n\n if (enableCursorSync && !options.serialize) {\n throw new Error('createCollabPlugins: cursorSync requires serialize to be provided')\n }\n const { doc, mapping: rawMapping } = initProseMirrorDoc(sharedProseMirror, schema)\n const mapping = rawMapping as ProseMirrorMapping\n const pmAwareness = enableCursorSync\n ? createAwarenessProxy(options.awareness, cursorFieldName)\n : options.awareness\n\n const plugins: Plugin[] = [\n ySyncPlugin(sharedProseMirror, { mapping: rawMapping }),\n yCursorPlugin(pmAwareness, options.yCursorPluginOpts ?? {}),\n yUndoPlugin(options.yUndoPluginOpts),\n ]\n\n // Bridge sync plugin must run before cursor sync plugin so that\n // Y.Text is updated before cursor positions are computed.\n if (options.bridge) {\n plugins.push(createBridgeSyncPlugin(options.bridge, { onWarning: options.onWarning }))\n }\n\n if (enableCursorSync && options.serialize) {\n plugins.push(\n createCursorSyncPlugin({\n awareness: options.awareness,\n serialize: options.serialize,\n cursorFieldName,\n cmCursorFieldName: options.cmCursorFieldName,\n sharedText: options.sharedText,\n onWarning: options.onWarning,\n }),\n )\n }\n\n return { plugins, doc, mapping }\n}\n","import { Plugin, PluginKey } from 'prosemirror-state'\nimport type { EditorView } from 'prosemirror-view'\nimport type { YjsBridgeHandle, OnWarning } from './types.js'\n\ntype BridgeSyncState = { needsSync: boolean }\n\ntype BridgeSyncFailure = { ok: false; reason: 'detached' | 'serialize-error' }\n\n/** Options for {@link createBridgeSyncPlugin}. */\nexport type BridgeSyncPluginOptions = {\n /** Called when `syncToSharedText` fails (excludes `reason: 'unchanged'`). */\n onSyncFailure?: (result: BridgeSyncFailure, view: EditorView) => void\n /** Called for non-fatal warnings. Default `console.warn`. */\n onWarning?: OnWarning\n}\n\n/** ProseMirror plugin key for {@link createBridgeSyncPlugin}. Use to read the plugin state. */\nexport const bridgeSyncPluginKey = new PluginKey<BridgeSyncState>('pm-cm-bridge-sync')\n\nconst wiredBridges = new WeakMap<YjsBridgeHandle, number>()\n\nconst defaultOnWarning: OnWarning = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`)\n\n/**\n * ProseMirror plugin that automatically syncs PM doc changes to Y.Text\n * via the bridge handle. Skips Yjs-originated changes to avoid loops.\n *\n * A warning is logged if the same bridge handle is wired more than once.\n * The guard is cleaned up when the plugin is destroyed.\n */\nexport function createBridgeSyncPlugin(\n bridge: YjsBridgeHandle,\n options: BridgeSyncPluginOptions = {},\n): Plugin {\n const warn = options.onWarning ?? defaultOnWarning\n\n // Tracks whether any Yjs-originated docChange was seen in the current\n // dispatch batch. Reset in view.update after each cycle.\n // appendTransaction plugins (e.g. prosemirror-tables) may emit follow-up\n // transactions that lack ySyncPlugin meta — this flag prevents those\n // from triggering a spurious text→PM→text round-trip.\n let yjsBatchSeen = false\n // Closure flag consumed in view.update — avoids sticky plugin state across dispatches.\n let needsSync = false\n\n return new Plugin<BridgeSyncState>({\n key: bridgeSyncPluginKey,\n\n state: {\n init(): BridgeSyncState {\n return { needsSync: false }\n },\n apply(tr, _prev): BridgeSyncState {\n if (!tr.docChanged) return { needsSync }\n if (bridge.isYjsSyncChange(tr)) {\n yjsBatchSeen = true\n needsSync = false\n return { needsSync: false }\n }\n if (yjsBatchSeen) {\n needsSync = false\n return { needsSync: false }\n }\n needsSync = true\n return { needsSync: true }\n },\n },\n\n view() {\n const count = wiredBridges.get(bridge) ?? 0\n if (count > 0) {\n warn({ code: 'bridge-already-wired', message: 'this bridge is already wired to another plugin instance' })\n }\n wiredBridges.set(bridge, count + 1)\n\n return {\n update(view) {\n if (needsSync) {\n const result = bridge.syncToSharedText(view.state.doc)\n if (!result.ok) {\n if (result.reason !== 'unchanged') {\n options.onSyncFailure?.(result, view)\n warn({ code: 'sync-failed', message: `bridge sync failed: ${result.reason}` })\n }\n }\n }\n needsSync = false\n yjsBatchSeen = false\n },\n destroy() {\n const remaining = (wiredBridges.get(bridge) ?? 1) - 1\n if (remaining <= 0) wiredBridges.delete(bridge)\n else wiredBridges.set(bridge, remaining)\n },\n }\n },\n })\n}\n","import { Plugin, PluginKey } from 'prosemirror-state'\nimport type { Node } from 'prosemirror-model'\nimport type { EditorView } from 'prosemirror-view'\nimport type { Awareness } from 'y-protocols/awareness'\nimport { absolutePositionToRelativePosition, ySyncPluginKey } from 'y-prosemirror'\nimport { createRelativePositionFromTypeIndex, createAbsolutePositionFromRelativePosition } from 'yjs'\nimport type { Text as YText, XmlFragment as YXmlFragment, RelativePosition } from 'yjs'\nimport type { Serialize, SerializeWithMap, CursorMap } from '@pm-cm/core'\nimport { buildCursorMap, cursorMapLookup, reverseCursorMapLookup } from '@pm-cm/core'\nimport type { OnWarning } from './types.js'\n\n/** Plugin state for the cursor sync plugin. Read via {@link cursorSyncPluginKey}. */\nexport type CursorSyncState = {\n /** Pending CodeMirror cursor to broadcast. Set by {@link syncCmCursor}. */\n pendingCm: { anchor: number; head: number } | null\n /** Text offset mapped from the current PM selection anchor. `null` when no mapping is available. */\n mappedTextOffset: number | null\n}\n\n/** ProseMirror plugin key for {@link createCursorSyncPlugin}. Use to read the plugin state. */\nexport const cursorSyncPluginKey = new PluginKey<CursorSyncState>('pm-cm-cursor-sync')\n\n/**\n * Internal shape of `ySyncPluginKey` state from y-prosemirror.\n * Not exported by upstream — kept here for explicit tracking.\n * Tested against y-prosemirror ^1.3.x.\n */\ntype YSyncPluginState = { type: YXmlFragment; binding: { mapping: Map<unknown, unknown> } }\n\nfunction getYSyncState(view: EditorView): YSyncPluginState | null {\n const raw = ySyncPluginKey.getState(view.state) as Record<string, unknown> | undefined\n if (!raw) return null\n if (\n typeof raw === 'object' &&\n 'type' in raw && raw.type &&\n 'binding' in raw && raw.binding &&\n typeof raw.binding === 'object' &&\n 'mapping' in (raw.binding as Record<string, unknown>) &&\n (raw.binding as Record<string, unknown>).mapping instanceof Map\n ) {\n return raw as unknown as YSyncPluginState\n }\n return null\n}\n\nfunction toRelativePosition(\n view: EditorView,\n pmPos: number,\n): unknown | null {\n const ySyncState = getYSyncState(view)\n if (!ySyncState) return null\n\n return absolutePositionToRelativePosition(\n pmPos,\n ySyncState.type,\n ySyncState.binding.mapping as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- y-prosemirror internal mapping type\n )\n}\n\n/** Returns `false` when ySyncPlugin state is unavailable (plugin not installed). */\nfunction broadcastPmCursor(\n awareness: Awareness,\n cursorFieldName: string,\n view: EditorView,\n pmAnchor: number,\n pmHead: number,\n): boolean {\n const relAnchor = toRelativePosition(view, pmAnchor)\n const relHead = toRelativePosition(view, pmHead)\n if (relAnchor === null || relHead === null) return false\n\n awareness.setLocalStateField(cursorFieldName, { anchor: relAnchor, head: relHead })\n return true\n}\n\nfunction broadcastTextCursor(\n awareness: Awareness,\n cmCursorFieldName: string,\n sharedText: YText,\n textAnchor: number,\n textHead: number,\n): void {\n const len = sharedText.length\n const clamp = (v: number) => Math.max(0, Math.min(v, len))\n const relAnchor = createRelativePositionFromTypeIndex(sharedText, clamp(textAnchor))\n const relHead = createRelativePositionFromTypeIndex(sharedText, clamp(textHead))\n awareness.setLocalStateField(cmCursorFieldName, { anchor: relAnchor, head: relHead })\n}\n\nconst defaultOnWarning: OnWarning = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`)\n\n/** Options for {@link createCursorSyncPlugin}. */\nexport type CursorSyncPluginOptions = {\n awareness: Awareness\n serialize: Serialize | SerializeWithMap\n cursorFieldName?: string\n /** Awareness field used for CM/Y.Text cursor payloads. Default `'cursor'`. */\n cmCursorFieldName?: string\n /**\n * When provided, the plugin also broadcasts CM-format cursor positions\n * (Y.Text relative positions) to the awareness field specified by\n * `cmCursorFieldName`, so that remote `yCollab` instances can render the cursor.\n */\n sharedText?: YText\n /** Called for non-fatal warnings. Default `console.warn`. */\n onWarning?: OnWarning\n}\n\n/**\n * ProseMirror plugin that synchronizes cursor positions between PM and CM via Yjs awareness.\n *\n * - PM → awareness: automatically broadcasts when the PM view is focused and selection changes.\n * - CM → awareness: triggered by dispatching {@link syncCmCursor}.\n */\nexport function createCursorSyncPlugin(options: CursorSyncPluginOptions): Plugin {\n const { awareness, serialize, sharedText } = options\n const warn = options.onWarning ?? defaultOnWarning\n const cursorFieldName = options.cursorFieldName ?? 'pmCursor'\n const cmCursorFieldName = options.cmCursorFieldName ?? 'cursor'\n\n if (sharedText && cursorFieldName === cmCursorFieldName) {\n throw new Error(\n `createCursorSyncPlugin: cursorFieldName and cmCursorFieldName must differ when sharedText is provided (both are \"${cursorFieldName}\")`,\n )\n }\n\n let warnedSyncPluginMissing = false\n\n // Closure variable consumed in view.update — survives appendTransaction\n // that would otherwise clear the plugin state before update() runs.\n let pendingCmCursor: { anchor: number; head: number } | null = null\n\n // Cached cursor map (serialize-based) — rebuilt only when doc changes\n let cachedMap: CursorMap | null = null\n let cachedMapDoc: Node | null = null\n\n function getOrBuildMap(doc: Node): CursorMap | null {\n if (cachedMapDoc !== doc || !cachedMap) {\n try {\n cachedMap = buildCursorMap(doc, serialize)\n } catch (error) {\n warn({ code: 'cursor-map-error', message: 'failed to build cursor map — cursor sync skipped' })\n cachedMap = null\n }\n cachedMapDoc = doc\n }\n return cachedMap\n }\n\n return new Plugin<CursorSyncState>({\n key: cursorSyncPluginKey,\n\n state: {\n init(): CursorSyncState {\n return { pendingCm: null, mappedTextOffset: null }\n },\n apply(tr, prev, _oldState, newState): CursorSyncState {\n const cmMeta = tr.getMeta(cursorSyncPluginKey) as\n | { anchor: number; head: number }\n | undefined\n if (cmMeta) {\n pendingCmCursor = cmMeta\n }\n\n // Compute PM → text offset when selection or doc changes\n let mappedTextOffset = prev.mappedTextOffset\n if (tr.selectionSet || tr.docChanged) {\n const map = getOrBuildMap(newState.doc)\n mappedTextOffset = map ? cursorMapLookup(map, newState.selection.anchor) : null\n }\n\n return {\n pendingCm: pendingCmCursor,\n mappedTextOffset,\n }\n },\n },\n\n view(editorView) {\n // Suppress awareness listener reactions when PM broadcasts to cmCursorFieldName\n let suppressCmReaction = false\n // Track last resolved CM cursor to avoid redundant pmCursor broadcasts\n let lastCmAbsAnchor = -1\n let lastCmAbsHead = -1\n // Set when the awareness listener already broadcast pmCursor for the\n // current CM cursor change, so pendingCm can skip its own broadcast.\n let cmCursorHandledByListener = false\n\n // When sharedText is available, yCollab writes the CM cursor (including\n // range selections) to awareness[cmCursorFieldName]. This listener\n // converts those Y.Text relative positions to PM positions and\n // broadcasts to pmCursor — so the app never needs to forward CM ranges.\n const handleAwarenessUpdate = (\n { added, updated }: { added: number[]; updated: number[]; removed: number[] },\n ) => {\n if (suppressCmReaction) return\n if (!sharedText?.doc) return\n const localId = awareness.clientID\n if (!updated.includes(localId) && !added.includes(localId)) return\n\n const localState = awareness.getLocalState() as Record<string, unknown> | null\n if (!localState) return\n\n const cmCursor = localState[cmCursorFieldName] as\n | { anchor: RelativePosition; head: RelativePosition }\n | undefined\n if (!cmCursor?.anchor || !cmCursor?.head) {\n // CM cursor was cleared — also clear pmCursor if it was previously set\n if (lastCmAbsAnchor !== -1) {\n awareness.setLocalStateField(cursorFieldName, null)\n lastCmAbsAnchor = -1\n lastCmAbsHead = -1\n }\n return\n }\n\n const absAnchor = createAbsolutePositionFromRelativePosition(cmCursor.anchor, sharedText.doc!)\n const absHead = createAbsolutePositionFromRelativePosition(cmCursor.head, sharedText.doc!)\n if (!absAnchor || !absHead) {\n // Relative position can't be resolved — clear stale PM cursor\n awareness.setLocalStateField(cursorFieldName, null)\n lastCmAbsAnchor = -1\n lastCmAbsHead = -1\n return\n }\n\n // Skip if unchanged (prevents loops from our own pmCursor broadcast)\n if (absAnchor.index === lastCmAbsAnchor && absHead.index === lastCmAbsHead) return\n\n const map = getOrBuildMap(editorView.state.doc)\n if (!map) {\n awareness.setLocalStateField(cursorFieldName, null)\n return\n }\n const pmAnchor = reverseCursorMapLookup(map, absAnchor.index)\n const pmHead = reverseCursorMapLookup(map, absHead.index)\n if (pmAnchor === null || pmHead === null) {\n // Mapping failed — clear stale PM cursor\n awareness.setLocalStateField(cursorFieldName, null)\n return\n }\n\n // Only update dedup cache after successful mapping — failed mapping\n // must not prevent retry when the doc changes and the map resolves.\n lastCmAbsAnchor = absAnchor.index\n lastCmAbsHead = absHead.index\n\n cmCursorHandledByListener = true\n const ok = broadcastPmCursor(awareness, cursorFieldName, editorView, pmAnchor, pmHead)\n if (!ok && !warnedSyncPluginMissing) {\n warnedSyncPluginMissing = true\n warn({ code: 'ysync-plugin-missing', message: 'ySyncPlugin state not available — cursor broadcast skipped' })\n }\n }\n\n if (sharedText) {\n awareness.on('update', handleAwarenessUpdate)\n }\n\n return {\n update(view, prevState) {\n // Invalidate dedup cache when doc changes so awareness listener\n // re-broadcasts pmCursor even if text offsets haven't changed.\n // Also re-trigger the handler to resolve cursors that failed mapping\n // when the doc was stale (no new awareness update would fire).\n if (view.state.doc !== prevState.doc) {\n lastCmAbsAnchor = -1\n lastCmAbsHead = -1\n if (sharedText?.doc && !suppressCmReaction) {\n handleAwarenessUpdate({ added: [awareness.clientID], updated: [], removed: [] })\n }\n }\n\n // CM → awareness: broadcast when pendingCmCursor is set.\n // Uses closure variable so appendTransaction cannot clear it.\n // If the awareness listener already converted yCollab's range to\n // pmCursor, skip here to avoid overwriting with collapsed data.\n if (pendingCmCursor != null) {\n const cursor = pendingCmCursor\n pendingCmCursor = null\n if (!cmCursorHandledByListener) {\n const map = getOrBuildMap(view.state.doc)\n const pmAnchor = map ? reverseCursorMapLookup(map, cursor.anchor) : null\n const pmHead = map ? reverseCursorMapLookup(map, cursor.head) : null\n if (pmAnchor !== null && pmHead !== null) {\n const ok = broadcastPmCursor(awareness, cursorFieldName, view, pmAnchor, pmHead)\n if (!ok && !warnedSyncPluginMissing) {\n warnedSyncPluginMissing = true\n warn({ code: 'ysync-plugin-missing', message: 'ySyncPlugin state not available — cursor broadcast skipped' })\n }\n // Also broadcast CM-format cursor so remote yCollab can render it.\n // (When yCollab is active the listener handles this; this path\n // covers the case where syncCmCursor is called without yCollab.)\n if (sharedText?.doc) {\n broadcastTextCursor(\n awareness,\n cmCursorFieldName,\n sharedText,\n cursor.anchor,\n cursor.head,\n )\n }\n } else {\n // Mapping failed — clear stale PM cursor\n awareness.setLocalStateField(cursorFieldName, null)\n }\n }\n cmCursorHandledByListener = false\n return\n }\n\n // PM → awareness: auto-broadcast on selection/doc change when focused.\n // suppressCmReaction wraps ALL awareness writes so the listener\n // does not echo stale CM cursor back to pmCursor.\n if (\n view.hasFocus() &&\n (view.state.selection !== prevState.selection ||\n view.state.doc !== prevState.doc)\n ) {\n suppressCmReaction = true\n try {\n const { anchor, head } = view.state.selection\n const ok = broadcastPmCursor(awareness, cursorFieldName, view, anchor, head)\n if (!ok && !warnedSyncPluginMissing) {\n warnedSyncPluginMissing = true\n warn({ code: 'ysync-plugin-missing', message: 'ySyncPlugin state not available — cursor broadcast skipped' })\n }\n // Also broadcast CM-format cursor so remote yCollab can render it.\n if (sharedText?.doc) {\n const map = getOrBuildMap(view.state.doc)\n const textAnchor = map ? cursorMapLookup(map, anchor) : null\n const textHead = map ? cursorMapLookup(map, head) : null\n if (textAnchor !== null && textHead !== null) {\n broadcastTextCursor(awareness, cmCursorFieldName, sharedText, textAnchor, textHead)\n } else {\n // Mapping failed — clear stale CM cursor\n awareness.setLocalStateField(cmCursorFieldName, null)\n }\n }\n } finally {\n suppressCmReaction = false\n }\n }\n\n // Always reset so a stale flag from a previous awareness update\n // cannot suppress a later syncCmCursor broadcast.\n cmCursorHandledByListener = false\n },\n destroy() {\n // Clear cursor fields so remote clients don't see ghost cursors\n awareness.setLocalStateField(cursorFieldName, null)\n if (sharedText) {\n awareness.setLocalStateField(cmCursorFieldName, null)\n awareness.off('update', handleAwarenessUpdate)\n }\n },\n }\n },\n })\n}\n\n/**\n * Dispatch a CodeMirror cursor offset (or range) to the cursor sync plugin.\n * The plugin will convert it to a ProseMirror position and broadcast via awareness.\n *\n * @param view - The ProseMirror EditorView that has the cursor sync plugin installed.\n * @param anchor - CodeMirror text offset for the anchor.\n * @param head - CodeMirror text offset for the head (defaults to `anchor` for a collapsed cursor).\n * @param onWarning - Optional warning callback. Default `console.warn`.\n */\nexport function syncCmCursor(view: EditorView, anchor: number, head?: number, onWarning?: OnWarning): void {\n if (!cursorSyncPluginKey.getState(view.state)) {\n (onWarning ?? defaultOnWarning)({ code: 'cursor-sync-not-installed', message: 'cursor sync plugin is not installed on this EditorView' })\n return\n }\n const sanitize = (v: number) => { const n = Math.floor(v); return Number.isFinite(n) ? Math.max(0, n) : 0 }\n view.dispatch(\n view.state.tr.setMeta(cursorSyncPluginKey, {\n anchor: sanitize(anchor),\n head: sanitize(head ?? anchor),\n }),\n )\n}\n","export { createYjsBridge, replaceSharedText, replaceSharedProseMirror } from './bridge.js'\nexport type { YjsBridgeOptions, ReplaceResult, ReplaceTextResult, ReplaceProseMirrorResult } from './bridge.js'\nexport { createAwarenessProxy } from './awareness-proxy.js'\nexport { createCollabPlugins } from './collab-plugins.js'\nexport type { CollabPluginsOptions, ProseMirrorMapping, YCursorPluginOpts, YUndoPluginOpts } from './collab-plugins.js'\nexport {\n ORIGIN_TEXT_TO_PM,\n ORIGIN_PM_TO_TEXT,\n ORIGIN_INIT,\n} from './types.js'\nexport type {\n BootstrapResult,\n YjsBridgeConfig,\n YjsBridgeHandle,\n WarningCode,\n WarningEvent,\n OnWarning,\n} from './types.js'\n\n// Cursor mapping re-exported from @pm-cm/core\nexport { buildCursorMap, cursorMapLookup, reverseCursorMapLookup } from '@pm-cm/core'\nexport type { TextSegment, CursorMap, SerializeWithMap, CursorMapWriter, Matcher, MatchResult, MatchRun } from '@pm-cm/core'\n\n// Bridge sync plugin (auto PM→Y.Text wiring)\nexport { createBridgeSyncPlugin, bridgeSyncPluginKey } from './bridge-sync-plugin.js'\nexport type { BridgeSyncPluginOptions } from './bridge-sync-plugin.js'\n\n// Cursor sync plugin\nexport { createCursorSyncPlugin, cursorSyncPluginKey, syncCmCursor } from './cursor-sync-plugin.js'\nexport type { CursorSyncState, CursorSyncPluginOptions } from './cursor-sync-plugin.js'\n\n// Re-export types from @pm-cm/core\nexport { createCursorMapWriter, wrapSerialize } from '@pm-cm/core'\nexport type { Serialize, Parse, Normalize, OnError, ErrorCode, ErrorEvent } from '@pm-cm/core'\n"],"mappings":"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","names":["doc","value","Plugin","PluginKey","ySyncPluginKey","defaultOnWarning","buildCursorMap","cursorMapLookup","reverseCursorMapLookup"]}
|