RangesTracker.js 30 KB

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