comment.js 5.1 KB

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