comment.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. // @ts-check
  2. const { RetainOp, InsertOp, RemoveOp } = require('./operation/scan_op')
  3. const Range = require('./range')
  4. /**
  5. * @typedef {import("./types").CommentRawData} CommentRawData
  6. * @typedef {import("./operation/text_operation")} TextOperation
  7. */
  8. class Comment {
  9. /**
  10. * @readonly
  11. * @type {ReadonlyArray<Range>}
  12. */
  13. ranges = []
  14. /**
  15. * @readonly
  16. * @type {boolean}
  17. */
  18. resolved = false
  19. /**
  20. * @param {ReadonlyArray<Range>} ranges
  21. * @param {boolean} [resolved]
  22. */
  23. constructor(ranges, resolved = false) {
  24. this.resolved = resolved
  25. this.ranges = this.mergeRanges(ranges)
  26. }
  27. /**
  28. *
  29. * @param {number} cursor
  30. * @param {number} length
  31. * @param {boolean} [extendComment]
  32. * @returns {Comment}
  33. */
  34. applyInsert(cursor, length, extendComment = false) {
  35. let existingRangeExtended = false
  36. const newRanges = []
  37. for (const commentRange of this.ranges) {
  38. if (cursor === commentRange.end) {
  39. // insert right after the comment
  40. if (extendComment) {
  41. newRanges.push(commentRange.extendBy(length))
  42. existingRangeExtended = true
  43. } else {
  44. newRanges.push(commentRange)
  45. }
  46. } else if (cursor === commentRange.start) {
  47. // insert at the start of the comment
  48. if (extendComment) {
  49. newRanges.push(commentRange.extendBy(length))
  50. existingRangeExtended = true
  51. } else {
  52. newRanges.push(commentRange.moveBy(length))
  53. }
  54. } else if (commentRange.startIsAfter(cursor)) {
  55. // insert before the comment
  56. newRanges.push(commentRange.moveBy(length))
  57. } else if (commentRange.containsCursor(cursor)) {
  58. // insert is inside the comment
  59. if (extendComment) {
  60. newRanges.push(commentRange.extendBy(length))
  61. existingRangeExtended = true
  62. } else {
  63. const [rangeUpToCursor, , rangeAfterCursor] = commentRange.insertAt(
  64. cursor,
  65. length
  66. )
  67. // use current commentRange for the part before the cursor
  68. newRanges.push(new Range(commentRange.pos, rangeUpToCursor.length))
  69. // add the part after the cursor as a new range
  70. newRanges.push(rangeAfterCursor)
  71. }
  72. } else {
  73. // insert is after the comment
  74. newRanges.push(commentRange)
  75. }
  76. }
  77. // if the insert is not inside any range, add a new range
  78. if (extendComment && !existingRangeExtended) {
  79. newRanges.push(new Range(cursor, length))
  80. }
  81. return new Comment(newRanges, this.resolved)
  82. }
  83. /**
  84. *
  85. * @param {Range} deletedRange
  86. * @returns {Comment}
  87. */
  88. applyDelete(deletedRange) {
  89. const newRanges = []
  90. for (const commentRange of this.ranges) {
  91. if (commentRange.overlaps(deletedRange)) {
  92. newRanges.push(commentRange.subtract(deletedRange))
  93. } else if (commentRange.startsAfter(deletedRange)) {
  94. newRanges.push(commentRange.moveBy(-deletedRange.length))
  95. } else {
  96. newRanges.push(commentRange)
  97. }
  98. }
  99. return new Comment(newRanges, this.resolved)
  100. }
  101. /**
  102. *
  103. * @param {TextOperation} operation
  104. * @param {string} commentId
  105. * @returns {Comment}
  106. */
  107. applyTextOperation(operation, commentId) {
  108. /** @type {Comment} */
  109. let comment = this
  110. let cursor = 0
  111. for (const op of operation.ops) {
  112. if (op instanceof RetainOp) {
  113. cursor += op.length
  114. } else if (op instanceof InsertOp) {
  115. comment = comment.applyInsert(
  116. cursor,
  117. op.insertion.length,
  118. op.commentIds?.includes(commentId)
  119. )
  120. cursor += op.insertion.length
  121. } else if (op instanceof RemoveOp) {
  122. comment = comment.applyDelete(new Range(cursor, op.length))
  123. }
  124. }
  125. return comment
  126. }
  127. isEmpty() {
  128. return this.ranges.length === 0
  129. }
  130. /**
  131. *
  132. * @returns {CommentRawData}
  133. */
  134. toRaw() {
  135. return {
  136. resolved: this.resolved,
  137. ranges: this.ranges.map(range => range.toRaw()),
  138. }
  139. }
  140. /**
  141. * @param {ReadonlyArray<Range>} ranges
  142. * @returns {ReadonlyArray<Range>}
  143. */
  144. mergeRanges(ranges) {
  145. /** @type {Range[]} */
  146. const mergedRanges = []
  147. const sortedRanges = [...ranges].sort((a, b) => a.start - b.start)
  148. for (const range of sortedRanges) {
  149. if (range.isEmpty()) {
  150. continue
  151. }
  152. const lastMerged = mergedRanges[mergedRanges.length - 1]
  153. if (lastMerged?.canMerge(range)) {
  154. mergedRanges[mergedRanges.length - 1] = lastMerged.merge(range)
  155. } else {
  156. mergedRanges.push(range)
  157. }
  158. }
  159. return mergedRanges
  160. }
  161. /**
  162. * @param {CommentRawData} rawComment
  163. * @returns {Comment}
  164. */
  165. static fromRaw(rawComment) {
  166. return new Comment(
  167. rawComment.ranges.map(range => Range.fromRaw(range)),
  168. rawComment.resolved
  169. )
  170. }
  171. }
  172. module.exports = Comment