From b116335f9dbde4f483c0b2b8e7bfca5d321c5dfc Mon Sep 17 00:00:00 2001 From: haoyuren <13851610112@163.com> Date: Thu, 12 Mar 2026 17:52:53 -0500 Subject: Add bidirectional file sync, OT system, comments, and real-time collaboration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement full Overleaf integration with Socket.IO v0.9 real-time sync: - FileSyncBridge for bidirectional temp dir ↔ Overleaf sync via chokidar + diff-match-patch - OT state machine, transform functions, and CM6 adapter for collaborative editing - Comment system with highlights, tooltips, and review panel - Project list, file tree management, and socket-based compilation - 3-layer loop prevention (write guards, content equality, debounce) Co-Authored-By: Claude Opus 4.6 --- src/main/otTransform.ts | 117 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/main/otTransform.ts (limited to 'src/main/otTransform.ts') diff --git a/src/main/otTransform.ts b/src/main/otTransform.ts new file mode 100644 index 0000000..0d05450 --- /dev/null +++ b/src/main/otTransform.ts @@ -0,0 +1,117 @@ +// OT transform functions for main process (mirror of renderer transform) +import type { OtOp } from './otTypes' +import { isInsert, isDelete, isComment } from './otTypes' + +export function transformOps( + ops1: OtOp[], + ops2: OtOp[] +): { left: OtOp[]; right: OtOp[] } { + let right = ops2 + + const newLeft: OtOp[] = [] + for (const op1 of ops1) { + let transformed = op1 + const newRight: OtOp[] = [] + for (const op2 of right) { + const { left: tl, right: tr } = transformOp(transformed, op2) + transformed = tl + newRight.push(tr) + } + newLeft.push(transformed) + right = newRight + } + + return { left: newLeft, right } +} + +function transformOp(op1: OtOp, op2: OtOp): { left: OtOp; right: OtOp } { + if (isInsert(op1) && isInsert(op2)) { + if (op1.p <= op2.p) { + return { left: op1, right: { ...op2, p: op2.p + op1.i.length } } + } else { + return { left: { ...op1, p: op1.p + op2.i.length }, right: op2 } + } + } + + if (isInsert(op1) && isDelete(op2)) { + if (op1.p <= op2.p) { + return { left: op1, right: { ...op2, p: op2.p + op1.i.length } } + } else if (op1.p >= op2.p + op2.d.length) { + return { left: { ...op1, p: op1.p - op2.d.length }, right: op2 } + } else { + return { left: { ...op1, p: op2.p }, right: op2 } + } + } + + if (isDelete(op1) && isInsert(op2)) { + if (op2.p <= op1.p) { + return { left: { ...op1, p: op1.p + op2.i.length }, right: op2 } + } else if (op2.p >= op1.p + op1.d.length) { + return { left: op1, right: { ...op2, p: op2.p - op1.d.length } } + } else { + return { left: op1, right: { ...op2, p: op2.p - op1.d.length } } + } + } + + if (isDelete(op1) && isDelete(op2)) { + if (op1.p >= op2.p + op2.d.length) { + return { + left: { ...op1, p: op1.p - op2.d.length }, + right: { ...op2, p: op2.p } + } + } else if (op2.p >= op1.p + op1.d.length) { + return { + left: op1, + right: { ...op2, p: op2.p - op1.d.length } + } + } else { + const overlapStart = Math.max(0, op2.p - op1.p) + const overlapEnd = Math.min(op1.d.length, op2.p + op2.d.length - op1.p) + let newOp1Text = op1.d + if (overlapEnd > overlapStart) { + newOp1Text = op1.d.slice(0, overlapStart) + op1.d.slice(overlapEnd) + } + + const overlapStart2 = Math.max(0, op1.p - op2.p) + const overlapEnd2 = Math.min(op2.d.length, op1.p + op1.d.length - op2.p) + let newOp2Text = op2.d + if (overlapEnd2 > overlapStart2) { + newOp2Text = op2.d.slice(0, overlapStart2) + op2.d.slice(overlapEnd2) + } + + const newP1 = op1.p <= op2.p ? op1.p : op1.p - (overlapEnd2 - overlapStart2) + const newP2 = op2.p <= op1.p ? op2.p : op2.p - (overlapEnd - overlapStart) + + return { + left: newOp1Text ? { d: newOp1Text, p: Math.max(0, newP1) } : { d: '', p: 0 }, + right: newOp2Text ? { d: newOp2Text, p: Math.max(0, newP2) } : { d: '', p: 0 } + } + } + } + + if (isComment(op1) || isComment(op2)) { + if (isComment(op1)) { + if (isInsert(op2) && op2.p <= op1.p) { + return { left: { ...op1, p: op1.p + op2.i.length }, right: op2 } + } + if (isDelete(op2) && op2.p < op1.p) { + const shift = Math.min(op2.d.length, op1.p - op2.p) + return { left: { ...op1, p: op1.p - shift }, right: op2 } + } + } + + if (isComment(op2)) { + if (isInsert(op1) && op1.p <= op2.p) { + return { left: op1, right: { ...op2, p: op2.p + op1.i.length } } + } + if (isDelete(op1) && op1.p < op2.p) { + const shift = Math.min(op1.d.length, op2.p - op1.p) + return { left: op1, right: { ...op2, p: op2.p - shift } } + } + } + + return { left: op1, right: op2 } + } + + return { left: op1, right: op2 } +} -- cgit v1.2.3