index.cjs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. /**
  2. * The purpose of this class is to track a set of inserts and deletes to a document, like
  3. * track changes in Word. We store these as a set of ShareJs style ranges:
  4. * {i: "foo", p: 42} # Insert 'foo' at offset 42
  5. * {d: "bar", p: 37} # Delete 'bar' at offset 37
  6. * We only track the inserts and deletes, not the whole document, but by being given all
  7. * updates that are applied to a document, we can update these appropriately.
  8. *
  9. * Note that the set of inserts and deletes we store applies to the document as-is at the moment.
  10. * So inserts correspond to text which is in the document, while deletes correspond to text which
  11. * is no longer there, so their lengths do not affect the position of later offsets.
  12. * E.g.
  13. * this is the current text of the document
  14. * |-----| |
  15. * {i: "current ", p:12} -^ ^- {d: "old ", p: 31}
  16. *
  17. * Track changes rules (should be consistent with Word):
  18. * * When text is inserted at a delete, the text goes to the left of the delete
  19. * I.e. "foo|bar" -> "foobaz|bar", where | is the delete, and 'baz' is inserted
  20. * * Deleting content flagged as 'inserted' does not create a new delete marker, it only
  21. * removes the insert marker. E.g.
  22. * * "abdefghijkl" -> "abfghijkl" when 'de' is deleted. No delete marker added
  23. * |---| <- inserted |-| <- inserted
  24. * * Deletes overlapping regular text and inserted text will insert a delete marker for the
  25. * regular text:
  26. * "abcdefghijkl" -> "abcdejkl" when 'fghi' is deleted
  27. * |----| |--||
  28. * ^- inserted 'bcdefg' \ ^- deleted 'hi'
  29. * \--inserted 'bcde'
  30. * * Deletes overlapping other deletes are merged. E.g.
  31. * "abcghijkl" -> "ahijkl" when 'bcg is deleted'
  32. * | <- delete 'def' | <- delete 'bcdefg'
  33. * * Deletes by another user will consume deletes by the first user
  34. * * Inserts by another user will not combine with inserts by the first user. If they are in the
  35. * middle of a previous insert by the first user, the original insert will be split into two.
  36. */
  37. class RangesTracker {
  38. constructor(changes, comments) {
  39. if (changes == null) {
  40. changes = []
  41. }
  42. this.changes = changes
  43. if (comments == null) {
  44. comments = []
  45. }
  46. this.comments = comments
  47. this.track_changes = false
  48. this.id_seed = RangesTracker.generateIdSeed()
  49. this.id_increment = 0
  50. this._dirtyState = {
  51. comment: {
  52. moved: {},
  53. removed: {},
  54. added: {},
  55. },
  56. change: {
  57. moved: {},
  58. removed: {},
  59. added: {},
  60. },
  61. }
  62. }
  63. getIdSeed() {
  64. return this.id_seed
  65. }
  66. setIdSeed(seed) {
  67. this.id_seed = seed
  68. this.id_increment = 0
  69. }
  70. static generateIdSeed() {
  71. // Generate a the first 18 characters of Mongo ObjectId, leaving 6 for the increment part
  72. // Reference: https://github.com/dreampulse/ObjectId.js/blob/master/src/main/javascript/Objectid.js
  73. const pid = Math.floor(Math.random() * 32767).toString(16)
  74. const machine = Math.floor(Math.random() * 16777216).toString(16)
  75. const timestamp = Math.floor(new Date().valueOf() / 1000).toString(16)
  76. return (
  77. '00000000'.substr(0, 8 - timestamp.length) +
  78. timestamp +
  79. '000000'.substr(0, 6 - machine.length) +
  80. machine +
  81. '0000'.substr(0, 4 - pid.length) +
  82. pid
  83. )
  84. }
  85. static generateId() {
  86. return this.generateIdSeed() + '000001'
  87. }
  88. newId() {
  89. this.id_increment++
  90. const increment = this.id_increment.toString(16)
  91. const id =
  92. this.id_seed + '000000'.substr(0, 6 - increment.length) + increment
  93. return id
  94. }
  95. getComment(commentId) {
  96. let comment = null
  97. for (const c of this.comments) {
  98. if (c.id === commentId) {
  99. comment = c
  100. break
  101. }
  102. }
  103. return comment
  104. }
  105. removeCommentId(commentId) {
  106. const comment = this.getComment(commentId)
  107. if (comment == null) {
  108. return
  109. }
  110. this.comments = this.comments.filter(c => c.id !== commentId)
  111. this._markAsDirty(comment, 'comment', 'removed')
  112. }
  113. moveCommentId(commentId, position, text) {
  114. for (const comment of this.comments) {
  115. if (comment.id === commentId) {
  116. comment.op.p = position
  117. comment.op.c = text
  118. this._markAsDirty(comment, 'comment', 'moved')
  119. }
  120. }
  121. }
  122. getChange(changeId) {
  123. let change = null
  124. for (const c of this.changes) {
  125. if (c.id === changeId) {
  126. change = c
  127. break
  128. }
  129. }
  130. return change
  131. }
  132. getChanges(ids) {
  133. const idSet = new Set(ids)
  134. return this.changes.filter(change => idSet.has(change.id))
  135. }
  136. removeChangeId(changeId) {
  137. this.removeChangeIds([changeId])
  138. }
  139. removeChangeIds(ids) {
  140. if (ids == null || ids.length === 0) {
  141. return
  142. }
  143. const idSet = new Set(ids)
  144. const remainingChanges = []
  145. for (const change of this.changes) {
  146. if (idSet.has(change.id)) {
  147. this._markAsDirty(change, 'change', 'removed')
  148. } else {
  149. remainingChanges.push(change)
  150. }
  151. }
  152. this.changes = remainingChanges
  153. }
  154. validate(text) {
  155. let content
  156. for (const change of this.changes) {
  157. if (change.op.i != null) {
  158. content = text.slice(change.op.p, change.op.p + change.op.i.length)
  159. if (content !== change.op.i) {
  160. throw new Error('insertion does not match text in document')
  161. }
  162. }
  163. }
  164. for (const comment of this.comments) {
  165. content = text.slice(comment.op.p, comment.op.p + comment.op.c.length)
  166. if (content !== comment.op.c) {
  167. throw new Error('comment does not match text in document')
  168. }
  169. }
  170. }
  171. applyOp(op, metadata) {
  172. if (metadata == null) {
  173. metadata = {}
  174. }
  175. if (metadata.ts == null) {
  176. metadata.ts = new Date()
  177. }
  178. // Apply an op that has been applied to the document to our changes to keep them up to date
  179. if (op.i != null) {
  180. this.applyInsertToChanges(op, metadata)
  181. this.applyInsertToComments(op)
  182. } else if (op.d != null) {
  183. this.applyDeleteToChanges(op, metadata)
  184. this.applyDeleteToComments(op)
  185. } else if (op.c != null) {
  186. this.addComment(op, metadata)
  187. } else {
  188. throw new Error('unknown op type')
  189. }
  190. }
  191. applyOps(ops, metadata) {
  192. if (metadata == null) {
  193. metadata = {}
  194. }
  195. for (const op of ops) {
  196. this.applyOp(op, metadata)
  197. }
  198. }
  199. addComment(op, metadata) {
  200. const existing = this.getComment(op.t)
  201. if (existing != null) {
  202. this.moveCommentId(op.t, op.p, op.c)
  203. } else {
  204. let comment
  205. this.comments.push(
  206. (comment = {
  207. id: op.t || this.newId(),
  208. op: {
  209. // Copy because we'll modify in place
  210. c: op.c,
  211. p: op.p,
  212. t: op.t,
  213. },
  214. metadata,
  215. })
  216. )
  217. this._markAsDirty(comment, 'comment', 'added')
  218. }
  219. }
  220. applyInsertToComments(op) {
  221. for (const comment of this.comments) {
  222. if (op.p <= comment.op.p) {
  223. comment.op.p += op.i.length
  224. this._markAsDirty(comment, 'comment', 'moved')
  225. } else if (op.p < comment.op.p + comment.op.c.length) {
  226. const offset = op.p - comment.op.p
  227. comment.op.c =
  228. comment.op.c.slice(0, +(offset - 1) + 1 || undefined) +
  229. op.i +
  230. comment.op.c.slice(offset)
  231. this._markAsDirty(comment, 'comment', 'moved')
  232. }
  233. }
  234. }
  235. applyDeleteToComments(op) {
  236. const opStart = op.p
  237. const opLength = op.d.length
  238. const opEnd = op.p + opLength
  239. for (const comment of this.comments) {
  240. const commentStart = comment.op.p
  241. const commentEnd = comment.op.p + comment.op.c.length
  242. const commentLength = commentEnd - commentStart
  243. if (opEnd <= commentStart) {
  244. // delete is fully before comment
  245. comment.op.p -= opLength
  246. this._markAsDirty(comment, 'comment', 'moved')
  247. } else if (opStart >= commentEnd) {
  248. // delete is fully after comment, nothing to do
  249. } else {
  250. // delete and comment overlap
  251. let remainingAfter, remainingBefore
  252. if (opStart <= commentStart) {
  253. remainingBefore = ''
  254. } else {
  255. remainingBefore = comment.op.c.slice(0, opStart - commentStart)
  256. }
  257. if (opEnd >= commentEnd) {
  258. remainingAfter = ''
  259. } else {
  260. remainingAfter = comment.op.c.slice(opEnd - commentStart)
  261. }
  262. // Check deleted content matches delete op
  263. const deletedComment = comment.op.c.slice(
  264. remainingBefore.length,
  265. commentLength - remainingAfter.length
  266. )
  267. const offset = Math.max(0, commentStart - opStart)
  268. const deletedOpContent = op.d
  269. .slice(offset)
  270. .slice(0, deletedComment.length)
  271. if (deletedComment !== deletedOpContent) {
  272. throw new Error('deleted content does not match comment content')
  273. }
  274. comment.op.p = Math.min(commentStart, opStart)
  275. comment.op.c = remainingBefore + remainingAfter
  276. this._markAsDirty(comment, 'comment', 'moved')
  277. }
  278. }
  279. }
  280. applyInsertToChanges(op, metadata) {
  281. let change
  282. const opStart = op.p
  283. const opLength = op.i.length
  284. const opEnd = op.p + opLength
  285. const undoing = !!op.u
  286. const fixedRemoveChange = op.fixedRemoveChange
  287. let alreadyMerged = false
  288. let previousChange = null
  289. const movedChanges = []
  290. const removeChanges = []
  291. const newChanges = []
  292. const trackedDeletesAtOpPosition = []
  293. for (let i = 0; i < this.changes.length; i++) {
  294. change = this.changes[i]
  295. const changeStart = change.op.p
  296. if (change.op.d != null) {
  297. // Shift any deletes after this along by the length of this insert
  298. if (opStart < changeStart) {
  299. change.op.p += opLength
  300. movedChanges.push(change)
  301. } else if (opStart === changeStart) {
  302. if (
  303. !(op.orderedRejections && alreadyMerged) &&
  304. undoing &&
  305. change.op.d.length >= op.i.length &&
  306. change.op.d.slice(0, op.i.length) === op.i
  307. ) {
  308. // If we are undoing, then we want to reject any existing tracked delete if we can.
  309. // Check if the insert matches the start of the delete, and just
  310. // remove it from the delete instead if so.
  311. change.op.d = change.op.d.slice(op.i.length)
  312. change.op.p += op.i.length
  313. if (change.op.d === '') {
  314. removeChanges.push(change)
  315. } else {
  316. movedChanges.push(change)
  317. }
  318. alreadyMerged = true
  319. if (op.orderedRejections) {
  320. // Any tracked delete that came before this tracked delete
  321. // rejection was moved after the incoming insert. Move them back
  322. // so that they appear before the tracked delete rejection.
  323. for (const trackedDelete of trackedDeletesAtOpPosition) {
  324. trackedDelete.op.p -= opLength
  325. }
  326. }
  327. } else {
  328. // We're not rejecting that tracked delete. Move it after the
  329. // insert.
  330. change.op.p += opLength
  331. movedChanges.push(change)
  332. // Keep track of tracked deletes that are at the same position as the
  333. // insert. If we find a tracked delete to reject, we'll want to
  334. // reposition them.
  335. if (!alreadyMerged) {
  336. trackedDeletesAtOpPosition.push(change)
  337. }
  338. }
  339. }
  340. } else if (change.op.i != null) {
  341. let offset
  342. const changeEnd = changeStart + change.op.i.length
  343. const isChangeOverlapping =
  344. opStart >= changeStart && opStart <= changeEnd
  345. // Only merge inserts if they are from the same user
  346. const isSameUser = metadata.user_id === change.metadata.user_id
  347. // If we are undoing, then our changes will be removed from any delete ops just after. In that case, if there is also
  348. // an insert op just before, then we shouldn't append it to this insert, but instead only cancel the following delete.
  349. // E.g.
  350. // foo|<--- about to insert 'b' here
  351. // inserted 'foo' --^ ^-- deleted 'bar'
  352. // should become just 'foo' not 'foob' (with the delete marker becoming just 'ar'), .
  353. const nextChange = this.changes[i + 1]
  354. const isOpAdjacentToNextDelete =
  355. nextChange != null &&
  356. nextChange.op.d != null &&
  357. op.p === changeEnd &&
  358. nextChange.op.p === op.p
  359. const willOpCancelNextDelete =
  360. undoing &&
  361. isOpAdjacentToNextDelete &&
  362. nextChange.op.d.slice(0, op.i.length) === op.i
  363. // If there is a delete at the start of the insert, and we're inserting
  364. // at the start, we SHOULDN'T merge since the delete acts as a partition.
  365. // The previous op will be the delete, but it's already been shifted by this insert
  366. //
  367. // I.e.
  368. // Originally: |-- existing insert --|
  369. // | <- existing delete at same offset
  370. //
  371. // Now: |-- existing insert --| <- not shifted yet
  372. // |-- this insert --|| <- existing delete shifted along to end of this op
  373. //
  374. // After: |-- existing insert --|
  375. // |-- this insert --|| <- existing delete
  376. //
  377. // Without the delete, the inserts would be merged.
  378. const isInsertBlockedByDelete =
  379. previousChange != null &&
  380. previousChange.op.d != null &&
  381. previousChange.op.p === opEnd
  382. // If the insert is overlapping another insert, either at the beginning in the middle or touching the end,
  383. // then we merge them into one.
  384. if (
  385. this.track_changes &&
  386. isChangeOverlapping &&
  387. !isInsertBlockedByDelete &&
  388. !alreadyMerged &&
  389. !willOpCancelNextDelete &&
  390. isSameUser
  391. ) {
  392. offset = opStart - changeStart
  393. change.op.i =
  394. change.op.i.slice(0, offset) + op.i + change.op.i.slice(offset)
  395. change.metadata.ts = metadata.ts
  396. alreadyMerged = true
  397. movedChanges.push(change)
  398. } else if (opStart <= changeStart) {
  399. // If we're fully before the other insert we can just shift the other insert by our length.
  400. // If they are touching, and should have been merged, they will have been above.
  401. // If not merged above, then it must be blocked by a delete, and will be after this insert, so we shift it along as well
  402. change.op.p += opLength
  403. movedChanges.push(change)
  404. } else if (
  405. (!isSameUser || !this.track_changes) &&
  406. changeStart < opStart &&
  407. opStart < changeEnd
  408. ) {
  409. // This user is inserting inside a change by another user, so we need to split the
  410. // other user's change into one before and after this one.
  411. offset = opStart - changeStart
  412. const beforeContent = change.op.i.slice(0, offset)
  413. const afterContent = change.op.i.slice(offset)
  414. // The existing change can become the 'before' change
  415. change.op.i = beforeContent
  416. movedChanges.push(change)
  417. // Create a new op afterwards
  418. const afterChange = {
  419. op: {
  420. i: afterContent,
  421. p: changeStart + offset + opLength,
  422. },
  423. metadata: {},
  424. }
  425. for (const key in change.metadata) {
  426. const value = change.metadata[key]
  427. afterChange.metadata[key] = value
  428. }
  429. newChanges.push(afterChange)
  430. }
  431. }
  432. previousChange = change
  433. }
  434. if (this.track_changes && !alreadyMerged) {
  435. this._addOp(op, metadata)
  436. }
  437. for ({ op, metadata } of newChanges) {
  438. this._addOp(op, metadata)
  439. }
  440. for (change of removeChanges) {
  441. if (fixedRemoveChange) {
  442. this._removeChange(change)
  443. } else {
  444. this._brokenRemoveChange(change)
  445. }
  446. }
  447. for (change of movedChanges) {
  448. this._markAsDirty(change, 'change', 'moved')
  449. }
  450. }
  451. applyDeleteToChanges(op, metadata) {
  452. const opStart = op.p
  453. const opLength = op.d.length
  454. const opEnd = op.p + opLength
  455. const removeChanges = []
  456. const fixedRemoveChange = op.fixedRemoveChange
  457. let movedChanges = []
  458. // We might end up modifying our delete op if it merges with existing deletes, or cancels out
  459. // with an existing insert. Since we might do multiple modifications, we record them and do
  460. // all the modifications after looping through the existing changes, so as not to mess up the
  461. // offset indexes as we go.
  462. const opModifications = []
  463. for (const change of this.changes) {
  464. let changeStart
  465. if (change.op.i != null) {
  466. changeStart = change.op.p
  467. const changeEnd = changeStart + change.op.i.length
  468. if (opEnd <= changeStart) {
  469. // Shift ops after us back by our length
  470. change.op.p -= opLength
  471. movedChanges.push(change)
  472. } else if (opStart >= changeEnd) {
  473. // Delete is after insert, nothing to do
  474. } else {
  475. // When the new delete overlaps an insert, we should remove the part of the insert that
  476. // is now deleted, and also remove the part of the new delete that overlapped. I.e.
  477. // the two cancel out where they overlap.
  478. let deleteRemainingAfter,
  479. deleteRemainingBefore,
  480. insertRemainingAfter,
  481. insertRemainingBefore
  482. if (opStart >= changeStart) {
  483. // |-- existing insert --|
  484. // insertRemainingBefore -> |.....||-- new delete --|
  485. deleteRemainingBefore = ''
  486. insertRemainingBefore = change.op.i.slice(0, opStart - changeStart)
  487. } else {
  488. // deleteRemainingBefore -> |.....||-- existing insert --|
  489. // |-- new delete --|
  490. deleteRemainingBefore = op.d.slice(0, changeStart - opStart)
  491. insertRemainingBefore = ''
  492. }
  493. if (opEnd <= changeEnd) {
  494. // |-- existing insert --|
  495. // |-- new delete --||.....| <- insertRemainingAfter
  496. deleteRemainingAfter = ''
  497. insertRemainingAfter = change.op.i.slice(opEnd - changeStart)
  498. } else {
  499. // |-- existing insert --||.....| <- deleteRemainingAfter
  500. // |-- new delete --|
  501. deleteRemainingAfter = op.d.slice(changeEnd - opStart)
  502. insertRemainingAfter = ''
  503. }
  504. const insertRemaining = insertRemainingBefore + insertRemainingAfter
  505. if (insertRemaining.length > 0) {
  506. change.op.i = insertRemaining
  507. change.op.p = Math.min(changeStart, opStart)
  508. movedChanges.push(change)
  509. } else {
  510. removeChanges.push(change)
  511. }
  512. // We know what we want to preserve of our delete op before (deleteRemainingBefore) and what we want to preserve
  513. // afterwards (deleteRemainingBefore). Now we need to turn that into a modification which deletes the
  514. // chunk in the middle not covered by these.
  515. const deleteRemovedLength =
  516. op.d.length -
  517. deleteRemainingBefore.length -
  518. deleteRemainingAfter.length
  519. const deleteRemovedStart = deleteRemainingBefore.length
  520. const modification = {
  521. d: op.d.slice(
  522. deleteRemovedStart,
  523. deleteRemovedStart + deleteRemovedLength
  524. ),
  525. p: deleteRemovedStart,
  526. }
  527. if (modification.d.length > 0) {
  528. opModifications.push(modification)
  529. }
  530. }
  531. } else if (change.op.d != null) {
  532. changeStart = change.op.p
  533. if (
  534. opEnd < changeStart ||
  535. (!this.track_changes && opEnd === changeStart)
  536. ) {
  537. // Shift ops after us back by our length.
  538. // If we're tracking changes, it must be strictly before, since we'll merge
  539. // below if they are touching. Otherwise, touching is fine.
  540. change.op.p -= opLength
  541. movedChanges.push(change)
  542. } else if (opStart <= changeStart && changeStart <= opEnd) {
  543. if (this.track_changes) {
  544. // If we overlap a delete, add it in our content, and delete the existing change.
  545. // It's easier to do it this way, rather than modifying the existing delete in case
  546. // we overlap many deletes and we'd need to track that. We have a workaround to
  547. // update the delete in place if possible below.
  548. const offset = changeStart - opStart
  549. opModifications.push({ i: change.op.d, p: offset })
  550. removeChanges.push(change)
  551. } else {
  552. change.op.p = opStart
  553. movedChanges.push(change)
  554. }
  555. }
  556. }
  557. }
  558. // Copy rather than modify because we still need to apply it to comments
  559. op = {
  560. p: op.p,
  561. d: this._applyOpModifications(op.d, opModifications),
  562. }
  563. for (const change of removeChanges) {
  564. // This is a bit of hack to avoid removing one delete and replacing it with another.
  565. // If we don't do this, it causes the UI to flicker
  566. if (
  567. op.d.length > 0 &&
  568. change.op.d != null &&
  569. op.p <= change.op.p &&
  570. change.op.p <= op.p + op.d.length
  571. ) {
  572. change.op.p = op.p
  573. change.op.d = op.d
  574. change.metadata = metadata
  575. movedChanges.push(change)
  576. op.d = '' // stop it being added
  577. } else {
  578. if (fixedRemoveChange) {
  579. this._removeChange(change)
  580. } else {
  581. this._brokenRemoveChange(change)
  582. }
  583. }
  584. }
  585. if (this.track_changes && op.d.length > 0) {
  586. this._addOp(op, metadata)
  587. } else {
  588. // It's possible that we deleted an insert between two other inserts. I.e.
  589. // If we delete 'user_2 insert' in:
  590. // |-- user_1 insert --||-- user_2 insert --||-- user_1 insert --|
  591. // it becomes:
  592. // |-- user_1 insert --||-- user_1 insert --|
  593. // We need to merge these together again
  594. const results = this._scanAndMergeAdjacentUpdates()
  595. movedChanges = movedChanges.concat(results.movedChanges)
  596. for (const change of results.removeChanges) {
  597. if (fixedRemoveChange) {
  598. this._removeChange(change)
  599. } else {
  600. this._brokenRemoveChange(change)
  601. }
  602. movedChanges = movedChanges.filter(c => c !== change)
  603. }
  604. }
  605. for (const change of movedChanges) {
  606. this._markAsDirty(change, 'change', 'moved')
  607. }
  608. }
  609. _addOp(op, metadata) {
  610. // Don't take a reference to the existing op since we'll modify this in place with future changes
  611. op = this._clone(op)
  612. // TODO: Remove this when the orderedRejections transition is over
  613. delete op.orderedRejections
  614. const change = {
  615. id: this.newId(),
  616. op,
  617. metadata: this._clone(metadata),
  618. }
  619. this.changes.push(change)
  620. // Keep ops in order of offset, with deletes before inserts
  621. this.changes.sort(function (c1, c2) {
  622. const result = c1.op.p - c2.op.p
  623. if (result !== 0) {
  624. return result
  625. } else if (c1.op.i != null && c2.op.d != null) {
  626. return 1
  627. } else if (c1.op.d != null && c2.op.i != null) {
  628. return -1
  629. } else {
  630. return 0
  631. }
  632. })
  633. this._markAsDirty(change, 'change', 'added')
  634. }
  635. _removeChange(change) {
  636. this.changes = this.changes.filter(c => c !== change)
  637. this._markAsDirty(change, 'change', 'removed')
  638. }
  639. _brokenRemoveChange(change) {
  640. this.changes = this.changes.filter(c => c.id !== change.id)
  641. this._markAsDirty(change, 'change', 'removed')
  642. }
  643. _applyOpModifications(content, opModifications) {
  644. // Put in descending position order, with deleting first if at the same offset
  645. // (Inserting first would modify the content that the delete will delete)
  646. opModifications.sort(function (a, b) {
  647. const result = b.p - a.p
  648. if (result !== 0) {
  649. return result
  650. } else if (a.i != null && b.d != null) {
  651. return 1
  652. } else if (a.d != null && b.i != null) {
  653. return -1
  654. } else {
  655. return 0
  656. }
  657. })
  658. for (const modification of opModifications) {
  659. if (modification.i != null) {
  660. content =
  661. content.slice(0, modification.p) +
  662. modification.i +
  663. content.slice(modification.p)
  664. } else if (modification.d != null) {
  665. if (
  666. content.slice(
  667. modification.p,
  668. modification.p + modification.d.length
  669. ) !== modification.d
  670. ) {
  671. throw new Error('deletion does not match text in document')
  672. }
  673. content =
  674. content.slice(0, modification.p) +
  675. content.slice(modification.p + modification.d.length)
  676. }
  677. }
  678. return content
  679. }
  680. _scanAndMergeAdjacentUpdates() {
  681. // This should only need calling when deleting an update between two
  682. // other updates. There's no other way to get two adjacent updates from the
  683. // same user, since they would be merged on insert.
  684. let previousChange = null
  685. const removeChanges = []
  686. const movedChanges = []
  687. for (const change of this.changes) {
  688. if (previousChange?.op.i != null && change.op.i != null) {
  689. const previousChangeEnd =
  690. previousChange.op.p + previousChange.op.i.length
  691. const previousChangeUserId = previousChange.metadata.user_id
  692. const changeStart = change.op.p
  693. const changeUserId = change.metadata.user_id
  694. if (
  695. previousChangeEnd === changeStart &&
  696. previousChangeUserId === changeUserId
  697. ) {
  698. removeChanges.push(change)
  699. previousChange.op.i += change.op.i
  700. movedChanges.push(previousChange)
  701. }
  702. } else if (
  703. previousChange?.op.d != null &&
  704. change.op.d != null &&
  705. previousChange?.op.p === change.op.p
  706. ) {
  707. // Merge adjacent deletes
  708. previousChange.op.d += change.op.d
  709. removeChanges.push(change)
  710. movedChanges.push(previousChange)
  711. } else {
  712. // Only update to the current change if we haven't removed it.
  713. previousChange = change
  714. }
  715. }
  716. return { movedChanges, removeChanges }
  717. }
  718. resetDirtyState() {
  719. this._dirtyState = {
  720. comment: {
  721. moved: {},
  722. removed: {},
  723. added: {},
  724. },
  725. change: {
  726. moved: {},
  727. removed: {},
  728. added: {},
  729. },
  730. }
  731. }
  732. getDirtyState() {
  733. return this._dirtyState
  734. }
  735. getTrackedDeletesLength() {
  736. let length = 0
  737. for (const change of this.changes) {
  738. if (change.op.d != null) {
  739. length += change.op.d.length
  740. }
  741. }
  742. return length
  743. }
  744. _markAsDirty(object, type, action) {
  745. this._dirtyState[type][action][object.id] = object
  746. }
  747. _clone(object) {
  748. const clone = {}
  749. for (const k in object) {
  750. const v = object[k]
  751. clone[k] = v
  752. }
  753. return clone
  754. }
  755. }
  756. module.exports = RangesTracker