RangesTracker.js 30 KB

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