range.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  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. * Does this range overlap another range?
  78. *
  79. * Overlapping means that the two ranges have at least one character in common
  80. *
  81. * @param {Range} other - the other range
  82. */
  83. overlaps(other) {
  84. return this.start < other.end && this.end > other.start
  85. }
  86. /**
  87. * Does this range overlap the start of another range?
  88. *
  89. * @param {Range} other - the other range
  90. */
  91. overlapsStart(other) {
  92. return this.start <= other.start && this.end > other.start
  93. }
  94. /**
  95. * Does this range overlap the end of another range?
  96. *
  97. * @param {Range} other - the other range
  98. */
  99. overlapsEnd(other) {
  100. return this.start < other.end && this.end >= other.end
  101. }
  102. /**
  103. * checks if the range touches a given range
  104. * @param {Range} range
  105. */
  106. touches(range) {
  107. return this.end === range.start || this.start === range.end
  108. }
  109. /**
  110. * @param {Range} range
  111. * @returns {Range}
  112. */
  113. subtract(range) {
  114. if (this.contains(range)) {
  115. return this.shrinkBy(range.length)
  116. }
  117. if (range.contains(this)) {
  118. return new Range(this.pos, 0)
  119. }
  120. if (range.overlaps(this)) {
  121. if (range.start < this.start) {
  122. const intersectedLength = range.end - this.start
  123. return new Range(range.pos, this.length - intersectedLength)
  124. } else {
  125. const intersectedLength = this.end - range.start
  126. return new Range(this.pos, this.length - intersectedLength)
  127. }
  128. }
  129. return new Range(this.pos, this.length)
  130. }
  131. /**
  132. * @param {Range} range
  133. * @returns {boolean}
  134. */
  135. canMerge(range) {
  136. return this.overlaps(range) || this.touches(range)
  137. }
  138. /**
  139. * @param {Range} range
  140. */
  141. merge(range) {
  142. if (!this.canMerge(range)) {
  143. throw new Error('Ranges cannot be merged')
  144. }
  145. const newPos = Math.min(this.pos, range.pos)
  146. const newEnd = Math.max(this.end, range.end)
  147. return new Range(newPos, newEnd - newPos)
  148. }
  149. /**
  150. * Moves the range by a given number
  151. * @param {number} length
  152. */
  153. moveBy(length) {
  154. return new Range(this.pos + length, this.length)
  155. }
  156. /**
  157. * Extends the range by a given number
  158. * @param {number} extensionLength
  159. */
  160. extendBy(extensionLength) {
  161. return new Range(this.pos, this.length + extensionLength)
  162. }
  163. /**
  164. * Shrinks the range by a given number
  165. * @param {number} shrinkLength
  166. */
  167. shrinkBy(shrinkLength) {
  168. const newLength = this.length - shrinkLength
  169. if (newLength < 0) {
  170. throw new Error('Cannot shrink range by more than its length')
  171. }
  172. return new Range(this.pos, newLength)
  173. }
  174. /**
  175. * Splits a range on the cursor and insert a range with the length provided
  176. * @param {number} cursor
  177. * @param {number} length
  178. * @returns {[Range, Range, Range]}
  179. */
  180. insertAt(cursor, length) {
  181. if (!this.containsCursor(cursor)) {
  182. throw new Error('The cursor must be contained in the range')
  183. }
  184. const rangeUpToCursor = new Range(this.pos, cursor - this.pos)
  185. const insertedRange = new Range(cursor, length)
  186. const rangeAfterCursor = new Range(
  187. cursor + length,
  188. this.length - rangeUpToCursor.length
  189. )
  190. return [rangeUpToCursor, insertedRange, rangeAfterCursor]
  191. }
  192. toRaw() {
  193. return {
  194. pos: this.pos,
  195. length: this.length,
  196. }
  197. }
  198. /**
  199. * @param {RawRange} raw
  200. * @return {Range}
  201. */
  202. static fromRaw(raw) {
  203. return new Range(raw.pos, raw.length)
  204. }
  205. /**
  206. * Splits a range into two ranges, at a given cursor
  207. * @param {number} cursor
  208. * @returns {[Range, Range]}
  209. */
  210. splitAt(cursor) {
  211. if (!this.containsCursor(cursor)) {
  212. throw new Error('The cursor must be contained in the range')
  213. }
  214. const rangeUpToCursor = new Range(this.pos, cursor - this.pos)
  215. const rangeAfterCursor = new Range(
  216. cursor,
  217. this.length - rangeUpToCursor.length
  218. )
  219. return [rangeUpToCursor, rangeAfterCursor]
  220. }
  221. /**
  222. * Returns the intersection of this range with another range
  223. *
  224. * @param {Range} other - the other range
  225. * @return {Range | null} the intersection or null if the intersection is empty
  226. */
  227. intersect(other) {
  228. if (this.contains(other)) {
  229. return other
  230. } else if (other.contains(this)) {
  231. return this
  232. } else if (other.overlapsStart(this)) {
  233. return new Range(this.pos, other.end - this.start)
  234. } else if (other.overlapsEnd(this)) {
  235. return new Range(other.pos, this.end - other.start)
  236. } else {
  237. return null
  238. }
  239. }
  240. }
  241. module.exports = Range