comment.js 4.6 KB

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