Merge pull request #35982 from overleaf/mg-history-ot-comments

HistoryOT - Fix comments being dropped and crashing on detached ranges

GitOrigin-RevId: 0906a55a51a6fda9ac828efcbe72989573161261
This commit is contained in:
Malik Glossop
2026-09-17 12:03:46 +00:00
committed by Copybot
parent 19d97a75c6
commit 863e33af78
10 changed files with 804 additions and 48 deletions
@@ -126,6 +126,25 @@ class EditOperationTransformer {
`Transform not implemented for ${a.constructor.name}○${b.constructor.name}`
)
}
/**
* Transform each operation in `as` against each operation in `bs`, priming
* both lists in place.
* @param {EditOperation[]} as - modified in place
* @param {EditOperation[]} bs - modified in place
*/
static transformMultiple(as, bs) {
for (let i = 0; i < as.length; ++i) {
for (let j = 0; j < bs.length; ++j) {
const [aPrime, bPrime] = EditOperationTransformer.transform(
as[i],
bs[j]
)
as[i] = aPrime
bs[j] = bPrime
}
}
}
}
/**
@@ -10,6 +10,7 @@ const DeleteCommentOperation = require('../../lib/operation/delete_comment_opera
const SetCommentStateOperation = require('../../lib/operation/set_comment_state_operation')
const Range = require('../../lib/range')
const EditNoOperation = require('../../lib/operation/edit_no_operation')
const StringFileData = require('../../lib/file_data/string_file_data')
describe('EditOperation', function () {
it('Cannot be instantiated', function () {
@@ -246,6 +247,35 @@ describe('EditOperationTransformer', function () {
expect(bPrime).to.be.an.instanceof(SetCommentStateOperation)
expect(bPrime.toJSON()).to.deep.eql(b.toJSON())
})
it('transformMultiple rebases two concurrent op lists so they converge', function () {
const base = 'AB'
// two sequential client edits, inserting at the start
const as = [
new TextOperation().insert('x').retain(2),
new TextOperation().insert('y').retain(3),
]
// two sequential server edits, inserting at the end
const bs = [
new TextOperation().retain(2).insert('1'),
new TextOperation().retain(3).insert('2'),
]
const asOrig = as.map(op => EditOperationBuilder.fromJSON(op.toJSON()))
const bsOrig = bs.map(op => EditOperationBuilder.fromJSON(op.toJSON()))
EditOperationTransformer.transformMultiple(as, bs)
const applySeq = ops => {
const file = new StringFileData(base)
for (const op of ops) {
file.edit(op)
}
return file.getContent()
}
// client edits then rebased server edits, and the mirror order, both converge
expect(applySeq([...asOrig, ...bs])).to.equal('yxAB12')
expect(applySeq([...bsOrig, ...as])).to.equal('yxAB12')
})
})
describe('EditOperationBuilder', function () {