range.js 4.6 KB

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