index.cjs 26 KB

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