range.js 4.4 KB

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