RangesTracker.js 28 KB

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