2024-02-01 06:31:11 -05:00
|
|
|
// @ts-check
|
2024-04-24 11:09:53 -04:00
|
|
|
|
|
|
|
const OError = require('@overleaf/o-error')
|
|
|
|
|
2024-08-07 10:51:44 -04:00
|
|
|
/**
|
|
|
|
* @typedef {import('./types').RawRange} RawRange
|
|
|
|
*/
|
|
|
|
|
2024-01-23 08:52:10 -05:00
|
|
|
class Range {
|
|
|
|
/**
|
2024-01-26 11:20:27 -05:00
|
|
|
* @param {number} pos
|
|
|
|
* @param {number} length
|
2024-01-23 08:52:10 -05:00
|
|
|
*/
|
|
|
|
constructor(pos, length) {
|
2024-04-24 11:09:53 -04:00
|
|
|
if (pos < 0 || length < 0) {
|
|
|
|
throw new OError('Invalid range', { pos, length })
|
|
|
|
}
|
2024-02-13 10:08:54 -05:00
|
|
|
/** @readonly */
|
2024-01-23 08:52:10 -05:00
|
|
|
this.pos = pos
|
2024-02-13 10:08:54 -05:00
|
|
|
/** @readonly */
|
2024-01-23 08:52:10 -05:00
|
|
|
this.length = length
|
|
|
|
}
|
|
|
|
|
2024-08-07 10:51:44 -04:00
|
|
|
/**
|
|
|
|
* @return {number}
|
|
|
|
*/
|
2024-02-05 08:19:45 -05:00
|
|
|
get start() {
|
2024-02-01 06:31:11 -05:00
|
|
|
return this.pos
|
|
|
|
}
|
|
|
|
|
2024-08-07 10:51:44 -04:00
|
|
|
/**
|
|
|
|
* @return {number}
|
|
|
|
*/
|
2024-02-05 08:19:45 -05:00
|
|
|
get end() {
|
2024-02-01 06:31:11 -05:00
|
|
|
return this.pos + this.length
|
|
|
|
}
|
|
|
|
|
2024-05-03 09:27:39 -04:00
|
|
|
/**
|
|
|
|
* Is this range equal to the given range?
|
|
|
|
*
|
|
|
|
* @param {Range} other
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
equals(other) {
|
|
|
|
return this.pos === other.pos && this.length === other.length
|
|
|
|
}
|
|
|
|
|
2024-02-01 06:31:11 -05:00
|
|
|
/**
|
|
|
|
* @param {Range} range
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
startsAfter(range) {
|
2024-02-05 08:19:45 -05:00
|
|
|
return this.start >= range.end
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {number} pos
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
2024-02-05 08:19:45 -05:00
|
|
|
startIsAfter(pos) {
|
|
|
|
return this.start > pos
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
isEmpty() {
|
|
|
|
return this.length === 0
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* checks if the range contains a given range
|
|
|
|
* @param {Range} range
|
|
|
|
*/
|
|
|
|
contains(range) {
|
2024-02-05 08:19:45 -05:00
|
|
|
return this.start <= range.start && this.end >= range.end
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-02-05 08:19:45 -05:00
|
|
|
* checks if the range contains a cursor (i.e. is not at the ends of the range)
|
|
|
|
* @param {number} cursor
|
2024-02-01 06:31:11 -05:00
|
|
|
*/
|
2024-02-05 08:19:45 -05:00
|
|
|
containsCursor(cursor) {
|
|
|
|
return this.start <= cursor && this.end >= cursor
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Range} range
|
|
|
|
*/
|
|
|
|
overlaps(range) {
|
2024-02-05 08:19:45 -05:00
|
|
|
return this.start < range.end && this.end > range.start
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* checks if the range touches a given range
|
|
|
|
* @param {Range} range
|
|
|
|
*/
|
|
|
|
touches(range) {
|
2024-02-05 08:19:45 -05:00
|
|
|
return this.end === range.start || this.start === range.end
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Range} range
|
2024-02-13 10:08:54 -05:00
|
|
|
* @returns {Range}
|
2024-02-01 06:31:11 -05:00
|
|
|
*/
|
|
|
|
subtract(range) {
|
|
|
|
if (this.contains(range)) {
|
2024-02-13 10:08:54 -05:00
|
|
|
return this.shrinkBy(range.length)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (range.contains(this)) {
|
2024-02-13 10:08:54 -05:00
|
|
|
return new Range(this.pos, 0)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (range.overlaps(this)) {
|
2024-02-05 08:19:45 -05:00
|
|
|
if (range.start < this.start) {
|
|
|
|
const intersectedLength = range.end - this.start
|
2024-02-13 10:08:54 -05:00
|
|
|
return new Range(range.pos, this.length - intersectedLength)
|
2024-02-01 06:31:11 -05:00
|
|
|
} else {
|
2024-02-05 08:19:45 -05:00
|
|
|
const intersectedLength = this.end - range.start
|
2024-02-13 10:08:54 -05:00
|
|
|
return new Range(this.pos, this.length - intersectedLength)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-13 10:08:54 -05:00
|
|
|
return new Range(this.pos, this.length)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Range} range
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
canMerge(range) {
|
|
|
|
return this.overlaps(range) || this.touches(range)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Range} range
|
|
|
|
*/
|
|
|
|
merge(range) {
|
|
|
|
if (!this.canMerge(range)) {
|
|
|
|
throw new Error('Ranges cannot be merged')
|
|
|
|
}
|
2024-02-05 08:19:45 -05:00
|
|
|
const newPos = Math.min(this.pos, range.pos)
|
|
|
|
const newEnd = Math.max(this.end, range.end)
|
2024-02-13 10:08:54 -05:00
|
|
|
|
|
|
|
return new Range(newPos, newEnd - newPos)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Moves the range by a given number
|
|
|
|
* @param {number} length
|
|
|
|
*/
|
|
|
|
moveBy(length) {
|
2024-02-13 10:08:54 -05:00
|
|
|
return new Range(this.pos + length, this.length)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extends the range by a given number
|
|
|
|
* @param {number} extensionLength
|
|
|
|
*/
|
|
|
|
extendBy(extensionLength) {
|
2024-02-13 10:08:54 -05:00
|
|
|
return new Range(this.pos, this.length + extensionLength)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Shrinks the range by a given number
|
|
|
|
* @param {number} shrinkLength
|
|
|
|
*/
|
|
|
|
shrinkBy(shrinkLength) {
|
|
|
|
const newLength = this.length - shrinkLength
|
|
|
|
|
|
|
|
if (newLength < 0) {
|
|
|
|
throw new Error('Cannot shrink range by more than its length')
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Range(this.pos, newLength)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-02-05 08:19:45 -05:00
|
|
|
* Splits a range on the cursor and insert a range with the length provided
|
|
|
|
* @param {number} cursor
|
2024-02-01 06:31:11 -05:00
|
|
|
* @param {number} length
|
|
|
|
* @returns {[Range, Range, Range]}
|
|
|
|
*/
|
2024-02-05 08:19:45 -05:00
|
|
|
insertAt(cursor, length) {
|
|
|
|
if (!this.containsCursor(cursor)) {
|
|
|
|
throw new Error('The cursor must be contained in the range')
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
2024-02-05 08:19:45 -05:00
|
|
|
const rangeUpToCursor = new Range(this.pos, cursor - this.pos)
|
|
|
|
const insertedRange = new Range(cursor, length)
|
|
|
|
const rangeAfterCursor = new Range(
|
|
|
|
cursor + length,
|
|
|
|
this.length - rangeUpToCursor.length
|
2024-02-01 06:31:11 -05:00
|
|
|
)
|
2024-02-05 08:19:45 -05:00
|
|
|
return [rangeUpToCursor, insertedRange, rangeAfterCursor]
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
2024-01-23 08:52:10 -05:00
|
|
|
toRaw() {
|
|
|
|
return {
|
|
|
|
pos: this.pos,
|
2024-01-26 11:20:27 -05:00
|
|
|
length: this.length,
|
2024-01-23 08:52:10 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-07 10:51:44 -04:00
|
|
|
/**
|
|
|
|
* @param {RawRange} raw
|
|
|
|
* @return {Range}
|
|
|
|
*/
|
2024-01-23 08:52:10 -05:00
|
|
|
static fromRaw(raw) {
|
|
|
|
return new Range(raw.pos, raw.length)
|
|
|
|
}
|
2024-02-05 08:20:11 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Splits a range into two ranges, at a given cursor
|
|
|
|
* @param {number} cursor
|
|
|
|
* @returns {[Range, Range]}
|
|
|
|
*/
|
|
|
|
splitAt(cursor) {
|
|
|
|
if (!this.containsCursor(cursor)) {
|
|
|
|
throw new Error('The cursor must be contained in the range')
|
|
|
|
}
|
|
|
|
const rangeUpToCursor = new Range(this.pos, cursor - this.pos)
|
|
|
|
const rangeAfterCursor = new Range(
|
|
|
|
cursor,
|
|
|
|
this.length - rangeUpToCursor.length
|
|
|
|
)
|
|
|
|
return [rangeUpToCursor, rangeAfterCursor]
|
|
|
|
}
|
2024-01-23 08:52:10 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Range
|