You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

276 lines
10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/// There are several things that positions can be mapped through.
/// Such objects conform to this interface.
export interface Mappable {
/// Map a position through this object. When given, `assoc` (should
/// be -1 or 1, defaults to 1) determines with which side the
/// position is associated, which determines in which direction to
/// move when a chunk of content is inserted at the mapped position.
map: (pos: number, assoc?: number) => number
/// Map a position, and return an object containing additional
/// information about the mapping. The result's `deleted` field tells
/// you whether the position was deleted (completely enclosed in a
/// replaced range) during the mapping. When content on only one side
/// is deleted, the position itself is only considered deleted when
/// `assoc` points in the direction of the deleted content.
mapResult: (pos: number, assoc?: number) => MapResult
}
// Recovery values encode a range index and an offset. They are
// represented as numbers, because tons of them will be created when
// mapping, for example, a large number of decorations. The number's
// lower 16 bits provide the index, the remaining bits the offset.
//
// Note: We intentionally don't use bit shift operators to en- and
// decode these, since those clip to 32 bits, which we might in rare
// cases want to overflow. A 64-bit float can represent 48-bit
// integers precisely.
const lower16 = 0xffff
const factor16 = Math.pow(2, 16)
function makeRecover(index: number, offset: number) { return index + offset * factor16 }
function recoverIndex(value: number) { return value & lower16 }
function recoverOffset(value: number) { return (value - (value & lower16)) / factor16 }
const DEL_BEFORE = 1, DEL_AFTER = 2, DEL_ACROSS = 4, DEL_SIDE = 8
/// An object representing a mapped position with extra
/// information.
export class MapResult {
/// @internal
constructor(
/// The mapped version of the position.
readonly pos: number,
/// @internal
readonly delInfo: number,
/// @internal
readonly recover: number | null
) {}
/// Tells you whether the position was deleted, that is, whether the
/// step removed the token on the side queried (via the `assoc`)
/// argument from the document.
get deleted() { return (this.delInfo & DEL_SIDE) > 0 }
/// Tells you whether the token before the mapped position was deleted.
get deletedBefore() { return (this.delInfo & (DEL_BEFORE | DEL_ACROSS)) > 0 }
/// True when the token after the mapped position was deleted.
get deletedAfter() { return (this.delInfo & (DEL_AFTER | DEL_ACROSS)) > 0 }
/// Tells whether any of the steps mapped through deletes across the
/// position (including both the token before and after the
/// position).
get deletedAcross() { return (this.delInfo & DEL_ACROSS) > 0 }
}
/// A map describing the deletions and insertions made by a step, which
/// can be used to find the correspondence between positions in the
/// pre-step version of a document and the same position in the
/// post-step version.
export class StepMap implements Mappable {
/// Create a position map. The modifications to the document are
/// represented as an array of numbers, in which each group of three
/// represents a modified chunk as `[start, oldSize, newSize]`.
constructor(
/// @internal
readonly ranges: readonly number[],
/// @internal
readonly inverted = false
) {
if (!ranges.length && StepMap.empty) return StepMap.empty
}
/// @internal
recover(value: number) {
let diff = 0, index = recoverIndex(value)
if (!this.inverted) for (let i = 0; i < index; i++)
diff += this.ranges[i * 3 + 2] - this.ranges[i * 3 + 1]
return this.ranges[index * 3] + diff + recoverOffset(value)
}
mapResult(pos: number, assoc = 1): MapResult { return this._map(pos, assoc, false) as MapResult }
map(pos: number, assoc = 1): number { return this._map(pos, assoc, true) as number }
/// @internal
_map(pos: number, assoc: number, simple: boolean) {
let diff = 0, oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2
for (let i = 0; i < this.ranges.length; i += 3) {
let start = this.ranges[i] - (this.inverted ? diff : 0)
if (start > pos) break
let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex], end = start + oldSize
if (pos <= end) {
let side = !oldSize ? assoc : pos == start ? -1 : pos == end ? 1 : assoc
let result = start + diff + (side < 0 ? 0 : newSize)
if (simple) return result
let recover = pos == (assoc < 0 ? start : end) ? null : makeRecover(i / 3, pos - start)
let del = pos == start ? DEL_AFTER : pos == end ? DEL_BEFORE : DEL_ACROSS
if (assoc < 0 ? pos != start : pos != end) del |= DEL_SIDE
return new MapResult(result, del, recover)
}
diff += newSize - oldSize
}
return simple ? pos + diff : new MapResult(pos + diff, 0, null)
}
/// @internal
touches(pos: number, recover: number) {
let diff = 0, index = recoverIndex(recover)
let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2
for (let i = 0; i < this.ranges.length; i += 3) {
let start = this.ranges[i] - (this.inverted ? diff : 0)
if (start > pos) break
let oldSize = this.ranges[i + oldIndex], end = start + oldSize
if (pos <= end && i == index * 3) return true
diff += this.ranges[i + newIndex] - oldSize
}
return false
}
/// Calls the given function on each of the changed ranges included in
/// this map.
forEach(f: (oldStart: number, oldEnd: number, newStart: number, newEnd: number) => void) {
let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2
for (let i = 0, diff = 0; i < this.ranges.length; i += 3) {
let start = this.ranges[i], oldStart = start - (this.inverted ? diff : 0), newStart = start + (this.inverted ? 0 : diff)
let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex]
f(oldStart, oldStart + oldSize, newStart, newStart + newSize)
diff += newSize - oldSize
}
}
/// Create an inverted version of this map. The result can be used to
/// map positions in the post-step document to the pre-step document.
invert() {
return new StepMap(this.ranges, !this.inverted)
}
/// @internal
toString() {
return (this.inverted ? "-" : "") + JSON.stringify(this.ranges)
}
/// Create a map that moves all positions by offset `n` (which may be
/// negative). This can be useful when applying steps meant for a
/// sub-document to a larger document, or vice-versa.
static offset(n: number) {
return n == 0 ? StepMap.empty : new StepMap(n < 0 ? [0, -n, 0] : [0, 0, n])
}
/// A StepMap that contains no changed ranges.
static empty = new StepMap([])
}
/// A mapping represents a pipeline of zero or more [step
/// maps](#transform.StepMap). It has special provisions for losslessly
/// handling mapping positions through a series of steps in which some
/// steps are inverted versions of earlier steps. (This comes up when
/// [rebasing](/docs/guide/#transform.rebasing) steps for
/// collaboration or history management.)
export class Mapping implements Mappable {
/// Create a new mapping with the given position maps.
constructor(
/// The step maps in this mapping.
readonly maps: StepMap[] = [],
/// @internal
public mirror?: number[],
/// The starting position in the `maps` array, used when `map` or
/// `mapResult` is called.
public from = 0,
/// The end position in the `maps` array.
public to = maps.length
) {}
/// Create a mapping that maps only through a part of this one.
slice(from = 0, to = this.maps.length) {
return new Mapping(this.maps, this.mirror, from, to)
}
/// @internal
copy() {
return new Mapping(this.maps.slice(), this.mirror && this.mirror.slice(), this.from, this.to)
}
/// Add a step map to the end of this mapping. If `mirrors` is
/// given, it should be the index of the step map that is the mirror
/// image of this one.
appendMap(map: StepMap, mirrors?: number) {
this.to = this.maps.push(map)
if (mirrors != null) this.setMirror(this.maps.length - 1, mirrors)
}
/// Add all the step maps in a given mapping to this one (preserving
/// mirroring information).
appendMapping(mapping: Mapping) {
for (let i = 0, startSize = this.maps.length; i < mapping.maps.length; i++) {
let mirr = mapping.getMirror(i)
this.appendMap(mapping.maps[i], mirr != null && mirr < i ? startSize + mirr : undefined)
}
}
/// Finds the offset of the step map that mirrors the map at the
/// given offset, in this mapping (as per the second argument to
/// `appendMap`).
getMirror(n: number): number | undefined {
if (this.mirror) for (let i = 0; i < this.mirror.length; i++)
if (this.mirror[i] == n) return this.mirror[i + (i % 2 ? -1 : 1)]
}
/// @internal
setMirror(n: number, m: number) {
if (!this.mirror) this.mirror = []
this.mirror.push(n, m)
}
/// Append the inverse of the given mapping to this one.
appendMappingInverted(mapping: Mapping) {
for (let i = mapping.maps.length - 1, totalSize = this.maps.length + mapping.maps.length; i >= 0; i--) {
let mirr = mapping.getMirror(i)
this.appendMap(mapping.maps[i].invert(), mirr != null && mirr > i ? totalSize - mirr - 1 : undefined)
}
}
/// Create an inverted version of this mapping.
invert() {
let inverse = new Mapping
inverse.appendMappingInverted(this)
return inverse
}
/// Map a position through this mapping.
map(pos: number, assoc = 1) {
if (this.mirror) return this._map(pos, assoc, true) as number
for (let i = this.from; i < this.to; i++)
pos = this.maps[i].map(pos, assoc)
return pos
}
/// Map a position through this mapping, returning a mapping
/// result.
mapResult(pos: number, assoc = 1) { return this._map(pos, assoc, false) as MapResult }
/// @internal
_map(pos: number, assoc: number, simple: boolean) {
let delInfo = 0
for (let i = this.from; i < this.to; i++) {
let map = this.maps[i], result = map.mapResult(pos, assoc)
if (result.recover != null) {
let corr = this.getMirror(i)
if (corr != null && corr > i && corr < this.to) {
i = corr
pos = this.maps[corr].recover(result.recover)
continue
}
}
delInfo |= result.delInfo
pos = result.pos
}
return simple ? pos : new MapResult(pos, delInfo, null)
}
}