diff --git a/client/main.ts b/client/main.ts index 966a962..49ea4ff 100644 --- a/client/main.ts +++ b/client/main.ts @@ -8,9 +8,16 @@ const textarea = document.querySelector("textarea")!; const pt = new PlainTextORDT(); let me: string | undefined = undefined; -pt.onevent = event => { +pt.onevent = (event, textIndex, affectedLength) => { if (event.at[0] === me) return; + + let start = textarea.selectionStart, + end = textarea.selectionEnd; + if (textIndex <= start) start += affectedLength; + if (textIndex <= end) end += affectedLength; + textarea.value = pt.render()[0]; + textarea.setSelectionRange(start, end); }; const socket = new WebSocket("/api/connect"); diff --git a/sync/ordt/plain-text.ts b/sync/ordt/plain-text.ts index 091be8b..9a25d37 100644 --- a/sync/ordt/plain-text.ts +++ b/sync/ordt/plain-text.ts @@ -9,7 +9,11 @@ export type PlainTextOperation = | { type: "delete" }; export class PlainTextORDT extends CausalTree { - onevent?: (op: CausalTreeOp) => void; + onevent?: ( + op: CausalTreeOp, + textIndex: number, + affectedLength: number, + ) => void; // caches for insert op <=> text transformation, // all these arrays should be the same length (SoA) @@ -21,12 +25,13 @@ export class PlainTextORDT extends CausalTree { override apply(op: AnyCausalTreeOp): number { const opIdx = super.apply(op); op = this.operations[opIdx]; - this.#applyCacheUpdate(op, opIdx); - this.onevent?.(op); + + const [idx, len] = this.#applyCacheUpdate(op, opIdx); + this.onevent?.(op, idx, len); return opIdx; } - #applyCacheUpdate(op: CausalTreeOp, opIdx: number) { + #applyCacheUpdate(op: CausalTreeOp, opIdx: number): [number, number] { for (let i = 0; i < this.opIndexCache.length; i++) { if (this.opIndexCache[i] >= opIdx) { this.opIndexCache[i] += 1; @@ -37,20 +42,36 @@ export class PlainTextORDT extends CausalTree { const parentCacheIdx = this.timestamps.findIndex(it => it === parentTimestamp); if (op.type === "insert") { - let idx = parentCacheIdx + 1; - for (; idx < this.timestamps.length; idx++) { - const curr = this.timestamps[idx]; + let cacheIdx = parentCacheIdx + 1; + for (; cacheIdx < this.timestamps.length; cacheIdx++) { + const curr = this.timestamps[cacheIdx]; if (timestampCompare(curr, op.at) < 0) break; } - this.sequences.splice(idx, 0, op.sequence); - this.timestamps.splice(idx, 0, op.at); - this.deleted.splice(idx, 0, false); - this.opIndexCache.splice(idx, 0, opIdx); + this.sequences.splice(cacheIdx, 0, op.sequence); + this.timestamps.splice(cacheIdx, 0, op.at); + this.deleted.splice(cacheIdx, 0, false); + this.opIndexCache.splice(cacheIdx, 0, opIdx); + + let textIndex = 0; + for (let i = 0; i < cacheIdx; i++) { + if (this.deleted[i]) continue; + textIndex += this.sequences[i].length; + } + return [textIndex, op.sequence.length]; } if (op.type === "delete" && parentCacheIdx !== -1) { this.deleted[parentCacheIdx] = true; + + let textIndex = 0; + for (let i = 0; i < parentCacheIdx; i++) { + if (this.deleted[i]) continue; + textIndex += this.sequences[i].length; + } + return [textIndex, -this.sequences[parentCacheIdx].length]; } + + return [-1, 0]; } findOpAtTextIndex(textIndex: number): number {