HistoryConversions.js 4.7 KB

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