HistoryConversions.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. // @ts-check
  2. const _ = require('lodash')
  3. const { isDelete } = require('./Utils')
  4. /**
  5. * @import { Comment, HistoryComment, HistoryRanges, HistoryTrackedChange } from './types'
  6. * @import { Ranges, TrackedChange } from './types'
  7. */
  8. /**
  9. * Convert editor ranges to history ranges
  10. *
  11. * @param {Ranges} ranges
  12. * @return {HistoryRanges}
  13. */
  14. function toHistoryRanges(ranges) {
  15. const changes = ranges.changes ?? []
  16. const comments = (ranges.comments ?? []).slice()
  17. // Changes are assumed to be sorted, but not comments
  18. comments.sort((a, b) => a.op.p - b.op.p)
  19. /**
  20. * This will allow us to go through comments at a different pace as we loop
  21. * through tracked changes
  22. */
  23. const commentsIterator = new CommentsIterator(comments)
  24. /**
  25. * Current offset between editor pos and history pos
  26. */
  27. let offset = 0
  28. /**
  29. * History comments that might overlap with the tracked change considered
  30. *
  31. * @type {HistoryComment[]}
  32. */
  33. let pendingComments = []
  34. /**
  35. * The final history comments generated
  36. *
  37. * @type {HistoryComment[]}
  38. */
  39. const historyComments = []
  40. /**
  41. * The final history tracked changes generated
  42. *
  43. * @type {HistoryTrackedChange[]}
  44. */
  45. const historyChanges = []
  46. for (const change of changes) {
  47. historyChanges.push(toHistoryChange(change, offset))
  48. // After this point, we're only interested in tracked deletes
  49. if (!isDelete(change.op)) {
  50. continue
  51. }
  52. // Fill pendingComments with new comments that start before this tracked
  53. // delete and might overlap
  54. for (const comment of commentsIterator.nextComments(change.op.p)) {
  55. pendingComments.push(toHistoryComment(comment, offset))
  56. }
  57. // Save comments that are fully before this tracked delete
  58. const newPendingComments = []
  59. for (const historyComment of pendingComments) {
  60. const commentEnd = historyComment.op.p + historyComment.op.c.length
  61. if (commentEnd <= change.op.p) {
  62. historyComments.push(historyComment)
  63. } else {
  64. newPendingComments.push(historyComment)
  65. }
  66. }
  67. pendingComments = newPendingComments
  68. // The rest of pending comments overlap with this tracked change. Adjust
  69. // their history length.
  70. for (const historyComment of pendingComments) {
  71. historyComment.op.hlen =
  72. (historyComment.op.hlen ?? historyComment.op.c.length) +
  73. change.op.d.length
  74. }
  75. // Adjust the offset
  76. offset += change.op.d.length
  77. }
  78. // Save the last pending comments
  79. for (const historyComment of pendingComments) {
  80. historyComments.push(historyComment)
  81. }
  82. // Save any comments that came after the last tracked change
  83. for (const comment of commentsIterator.nextComments()) {
  84. historyComments.push(toHistoryComment(comment, offset))
  85. }
  86. const historyRanges = {}
  87. if (historyComments.length > 0) {
  88. historyRanges.comments = historyComments
  89. }
  90. if (historyChanges.length > 0) {
  91. historyRanges.changes = historyChanges
  92. }
  93. return historyRanges
  94. }
  95. class CommentsIterator {
  96. /**
  97. * Build a CommentsIterator
  98. *
  99. * @param {Comment[]} comments
  100. */
  101. constructor(comments) {
  102. this.comments = comments
  103. this.currentIndex = 0
  104. }
  105. /**
  106. * Generator that returns the next comments to consider
  107. *
  108. * @param {number} beforePos - only return comments that start before this position
  109. * @return {Iterable<Comment>}
  110. */
  111. *nextComments(beforePos = Infinity) {
  112. while (this.currentIndex < this.comments.length) {
  113. const comment = this.comments[this.currentIndex]
  114. if (comment.op.p < beforePos) {
  115. yield comment
  116. this.currentIndex += 1
  117. } else {
  118. return
  119. }
  120. }
  121. }
  122. }
  123. /**
  124. * Convert an editor tracked change into a history tracked change
  125. *
  126. * @param {TrackedChange} change
  127. * @param {number} offset - how much the history change is ahead of the
  128. * editor change
  129. * @return {HistoryTrackedChange}
  130. */
  131. function toHistoryChange(change, offset) {
  132. /** @type {HistoryTrackedChange} */
  133. const historyChange = _.cloneDeep(change)
  134. if (offset > 0) {
  135. historyChange.op.hpos = change.op.p + offset
  136. }
  137. return historyChange
  138. }
  139. /**
  140. * Convert an editor comment into a history comment
  141. *
  142. * @param {Comment} comment
  143. * @param {number} offset - how much the history comment is ahead of the
  144. * editor comment
  145. * @return {HistoryComment}
  146. */
  147. function toHistoryComment(comment, offset) {
  148. /** @type {HistoryComment} */
  149. const historyComment = _.cloneDeep(comment)
  150. if (offset > 0) {
  151. historyComment.op.hpos = comment.op.p + offset
  152. }
  153. return historyComment
  154. }
  155. module.exports = {
  156. toHistoryRanges,
  157. }