range.js 4.7 KB

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