mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Merge pull request #26209 from overleaf/em-multiple-edit-ops
Support multiple ops in the history OT ShareJS type GitOrigin-RevId: fad1e9081ed1978de414c5130692d3b23fcd13d8
This commit is contained in:
@@ -30,18 +30,49 @@ export const historyOTType = {
|
||||
api,
|
||||
|
||||
transformX(ops1: EditOperation[], ops2: EditOperation[]) {
|
||||
const [a, b] = EditOperationTransformer.transform(ops1[0], ops2[0])
|
||||
return [[a], [b]]
|
||||
// Dynamic programming algorithm: gradually transform both sides in a nested
|
||||
// loop.
|
||||
const left = [...ops1]
|
||||
const right = [...ops2]
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
for (let j = 0; j < right.length; j++) {
|
||||
// At this point:
|
||||
// left[0..i] is ops1[0..i] rebased over ops2[0..j-1]
|
||||
// right[0..j] is ops2[0..j] rebased over ops1[0..i-1]
|
||||
const [a, b] = EditOperationTransformer.transform(left[i], right[j])
|
||||
left[i] = a
|
||||
right[j] = b
|
||||
}
|
||||
}
|
||||
return [left, right]
|
||||
},
|
||||
|
||||
apply(snapshot: StringFileData, ops: EditOperation[]) {
|
||||
const afterFile = StringFileData.fromRaw(snapshot.toRaw())
|
||||
afterFile.edit(ops[0])
|
||||
for (const op of ops) {
|
||||
afterFile.edit(op)
|
||||
}
|
||||
return afterFile
|
||||
},
|
||||
|
||||
compose(ops1: EditOperation[], ops2: EditOperation[]) {
|
||||
return [ops1[0].compose(ops2[0])]
|
||||
const ops = [...ops1, ...ops2]
|
||||
let currentOp = ops.shift()
|
||||
if (currentOp === undefined) {
|
||||
// No ops to process
|
||||
return []
|
||||
}
|
||||
const result = []
|
||||
for (const op of ops) {
|
||||
if (currentOp.canBeComposedWith(op)) {
|
||||
currentOp = currentOp.compose(op)
|
||||
} else {
|
||||
result.push(currentOp)
|
||||
currentOp = op
|
||||
}
|
||||
}
|
||||
result.push(currentOp)
|
||||
return result
|
||||
},
|
||||
|
||||
// Do not provide normalize, used by submitOp to fixup bad input.
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { expect } from 'chai'
|
||||
import {
|
||||
StringFileData,
|
||||
TextOperation,
|
||||
AddCommentOperation,
|
||||
Range,
|
||||
} from 'overleaf-editor-core'
|
||||
import { historyOTType } from '@/features/ide-react/editor/share-js-history-ot-type'
|
||||
|
||||
describe('historyOTType', function () {
|
||||
let snapshot: StringFileData
|
||||
let opsA: TextOperation[]
|
||||
let opsB: TextOperation[]
|
||||
|
||||
beforeEach(function () {
|
||||
snapshot = new StringFileData('one plus two equals three')
|
||||
|
||||
// After opsA: "seven plus five equals twelve"
|
||||
opsA = [new TextOperation(), new TextOperation(), new TextOperation()]
|
||||
|
||||
opsA[0].remove(3)
|
||||
opsA[0].insert('seven')
|
||||
opsA[0].retain(22)
|
||||
|
||||
opsA[1].retain(11)
|
||||
opsA[1].remove(3)
|
||||
opsA[1].insert('five')
|
||||
opsA[1].retain(13)
|
||||
|
||||
opsA[2].retain(23)
|
||||
opsA[2].remove(5)
|
||||
opsA[2].insert('twelve')
|
||||
|
||||
// After ops2: "one times two equals two"
|
||||
opsB = [new TextOperation(), new TextOperation()]
|
||||
|
||||
opsB[0].retain(4)
|
||||
opsB[0].remove(4)
|
||||
opsB[0].insert('times')
|
||||
opsB[0].retain(17)
|
||||
|
||||
opsB[1].retain(21)
|
||||
opsB[1].remove(5)
|
||||
opsB[1].insert('two')
|
||||
})
|
||||
|
||||
describe('apply', function () {
|
||||
it('supports an empty operations array', function () {
|
||||
const result = historyOTType.apply(snapshot, [])
|
||||
expect(result.getContent()).to.equal('one plus two equals three')
|
||||
})
|
||||
|
||||
it('applies operations to the snapshot (opsA)', function () {
|
||||
const result = historyOTType.apply(snapshot, opsA)
|
||||
expect(result.getContent()).to.equal('seven plus five equals twelve')
|
||||
})
|
||||
|
||||
it('applies operations to the snapshot (opsB)', function () {
|
||||
const result = historyOTType.apply(snapshot, opsB)
|
||||
expect(result.getContent()).to.equal('one times two equals two')
|
||||
})
|
||||
})
|
||||
|
||||
describe('compose', function () {
|
||||
it('supports empty operations', function () {
|
||||
const ops = historyOTType.compose([], [])
|
||||
expect(ops).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('supports an empty operation on the left', function () {
|
||||
const ops = historyOTType.compose([], opsA)
|
||||
const result = historyOTType.apply(snapshot, ops)
|
||||
expect(result.getContent()).to.equal('seven plus five equals twelve')
|
||||
})
|
||||
|
||||
it('supports an empty operation on the right', function () {
|
||||
const ops = historyOTType.compose(opsA, [])
|
||||
const result = historyOTType.apply(snapshot, ops)
|
||||
expect(result.getContent()).to.equal('seven plus five equals twelve')
|
||||
})
|
||||
|
||||
it('supports operations on both sides', function () {
|
||||
const ops = historyOTType.compose(opsA.slice(0, 2), opsA.slice(2))
|
||||
const result = historyOTType.apply(snapshot, ops)
|
||||
expect(ops.length).to.equal(1)
|
||||
expect(result.getContent()).to.equal('seven plus five equals twelve')
|
||||
})
|
||||
|
||||
it("supports operations that can't be composed", function () {
|
||||
const comment = new AddCommentOperation('comment-id', [new Range(3, 10)])
|
||||
const ops = historyOTType.compose(opsA.slice(0, 2), [
|
||||
comment,
|
||||
...opsA.slice(2),
|
||||
])
|
||||
expect(ops.length).to.equal(3)
|
||||
const result = historyOTType.apply(snapshot, ops)
|
||||
expect(result.getContent()).to.equal('seven plus five equals twelve')
|
||||
})
|
||||
})
|
||||
|
||||
describe('transformX', function () {
|
||||
it('supports empty operations', function () {
|
||||
const [aPrime, bPrime] = historyOTType.transformX([], [])
|
||||
expect(aPrime).to.deep.equal([])
|
||||
expect(bPrime).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('supports an empty operation on the left', function () {
|
||||
const [aPrime, bPrime] = historyOTType.transformX([], opsB)
|
||||
expect(aPrime).to.deep.equal([])
|
||||
expect(bPrime).to.deep.equal(opsB)
|
||||
})
|
||||
|
||||
it('supports an empty operation on the right', function () {
|
||||
const [aPrime, bPrime] = historyOTType.transformX(opsA, [])
|
||||
expect(aPrime).to.deep.equal(opsA)
|
||||
expect(bPrime).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('supports operations on both sides (a then b)', function () {
|
||||
const [, bPrime] = historyOTType.transformX(opsA, opsB)
|
||||
const ops = historyOTType.compose(opsA, bPrime)
|
||||
const result = historyOTType.apply(snapshot, ops)
|
||||
expect(result.getContent()).to.equal('seven times five equals twelvetwo')
|
||||
})
|
||||
|
||||
it('supports operations on both sides (b then a)', function () {
|
||||
const [aPrime] = historyOTType.transformX(opsA, opsB)
|
||||
const ops = historyOTType.compose(opsB, aPrime)
|
||||
const result = historyOTType.apply(snapshot, ops)
|
||||
expect(result.getContent()).to.equal('seven times five equals twelvetwo')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user