range.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. // @ts-check
  2. class Range {
  3. /**
  4. * @param {number} pos
  5. * @param {number} length
  6. */
  7. constructor(pos, length) {
  8. /** @readonly */
  9. this.pos = pos
  10. /** @readonly */
  11. this.length = length
  12. }
  13. get start() {
  14. return this.pos
  15. }
  16. get end() {
  17. return this.pos + this.length
  18. }
  19. /**
  20. * @param {Range} range
  21. * @returns {boolean}
  22. */
  23. startsAfter(range) {
  24. return this.start >= range.end
  25. }
  26. /**
  27. * @param {number} pos
  28. * @returns {boolean}
  29. */
  30. startIsAfter(pos) {
  31. return this.start > pos
  32. }
  33. /**
  34. *
  35. * @returns {boolean}
  36. */
  37. isEmpty() {
  38. return this.length === 0
  39. }
  40. /**
  41. * checks if the range contains a given range
  42. * @param {Range} range
  43. */
  44. contains(range) {
  45. return this.start <= range.start && this.end >= range.end
  46. }
  47. /**
  48. * checks if the range contains a cursor (i.e. is not at the ends of the range)
  49. * @param {number} cursor
  50. */
  51. containsCursor(cursor) {
  52. return this.start <= cursor && this.end >= cursor
  53. }
  54. /**
  55. * @param {Range} range
  56. */
  57. overlaps(range) {
  58. return this.start < range.end && this.end > range.start
  59. }
  60. /**
  61. * checks if the range touches a given range
  62. * @param {Range} range
  63. */
  64. touches(range) {
  65. return this.end === range.start || this.start === range.end
  66. }
  67. /**
  68. * @param {Range} range
  69. * @returns {Range}
  70. */
  71. subtract(range) {
  72. if (this.contains(range)) {
  73. return this.shrinkBy(range.length)
  74. }
  75. if (range.contains(this)) {
  76. return new Range(this.pos, 0)
  77. }
  78. if (range.overlaps(this)) {
  79. if (range.start < this.start) {
  80. const intersectedLength = range.end - this.start
  81. return new Range(range.pos, this.length - intersectedLength)
  82. } else {
  83. const intersectedLength = this.end - range.start
  84. return new Range(this.pos, this.length - intersectedLength)
  85. }
  86. }
  87. return new Range(this.pos, this.length)
  88. }
  89. /**
  90. * @param {Range} range
  91. * @returns {boolean}
  92. */
  93. canMerge(range) {
  94. return this.overlaps(range) || this.touches(range)
  95. }
  96. /**
  97. * @param {Range} range
  98. */
  99. merge(range) {
  100. if (!this.canMerge(range)) {
  101. throw new Error('Ranges cannot be merged')
  102. }
  103. const newPos = Math.min(this.pos, range.pos)
  104. const newEnd = Math.max(this.end, range.end)
  105. return new Range(newPos, newEnd - newPos)
  106. }
  107. /**
  108. * Moves the range by a given number
  109. * @param {number} length
  110. */
  111. moveBy(length) {
  112. return new Range(this.pos + length, this.length)
  113. }
  114. /**
  115. * Extends the range by a given number
  116. * @param {number} extensionLength
  117. */
  118. extendBy(extensionLength) {
  119. return new Range(this.pos, this.length + extensionLength)
  120. }
  121. /**
  122. * Shrinks the range by a given number
  123. * @param {number} shrinkLength
  124. */
  125. shrinkBy(shrinkLength) {
  126. const newLength = this.length - shrinkLength
  127. if (newLength < 0) {
  128. throw new Error('Cannot shrink range by more than its length')
  129. }
  130. return new Range(this.pos, newLength)
  131. }
  132. /**
  133. * Splits a range on the cursor and insert a range with the length provided
  134. * @param {number} cursor
  135. * @param {number} length
  136. * @returns {[Range, Range, Range]}
  137. */
  138. insertAt(cursor, length) {
  139. if (!this.containsCursor(cursor)) {
  140. throw new Error('The cursor must be contained in the range')
  141. }
  142. const rangeUpToCursor = new Range(this.pos, cursor - this.pos)
  143. const insertedRange = new Range(cursor, length)
  144. const rangeAfterCursor = new Range(
  145. cursor + length,
  146. this.length - rangeUpToCursor.length
  147. )
  148. return [rangeUpToCursor, insertedRange, rangeAfterCursor]
  149. }
  150. toRaw() {
  151. return {
  152. pos: this.pos,
  153. length: this.length,
  154. }
  155. }
  156. static fromRaw(raw) {
  157. return new Range(raw.pos, raw.length)
  158. }
  159. /**
  160. * Splits a range into two ranges, at a given cursor
  161. * @param {number} cursor
  162. * @returns {[Range, Range]}
  163. */
  164. splitAt(cursor) {
  165. if (!this.containsCursor(cursor)) {
  166. throw new Error('The cursor must be contained in the range')
  167. }
  168. const rangeUpToCursor = new Range(this.pos, cursor - this.pos)
  169. const rangeAfterCursor = new Range(
  170. cursor,
  171. this.length - rangeUpToCursor.length
  172. )
  173. return [rangeUpToCursor, rangeAfterCursor]
  174. }
  175. }
  176. module.exports = Range