map.ts (10962B)
1 /// There are several things that positions can be mapped through. 2 /// Such objects conform to this interface. 3 export interface Mappable { 4 /// Map a position through this object. When given, `assoc` (should 5 /// be -1 or 1, defaults to 1) determines with which side the 6 /// position is associated, which determines in which direction to 7 /// move when a chunk of content is inserted at the mapped position. 8 map: (pos: number, assoc?: number) => number 9 10 /// Map a position, and return an object containing additional 11 /// information about the mapping. The result's `deleted` field tells 12 /// you whether the position was deleted (completely enclosed in a 13 /// replaced range) during the mapping. When content on only one side 14 /// is deleted, the position itself is only considered deleted when 15 /// `assoc` points in the direction of the deleted content. 16 mapResult: (pos: number, assoc?: number) => MapResult 17 } 18 19 // Recovery values encode a range index and an offset. They are 20 // represented as numbers, because tons of them will be created when 21 // mapping, for example, a large number of decorations. The number's 22 // lower 16 bits provide the index, the remaining bits the offset. 23 // 24 // Note: We intentionally don't use bit shift operators to en- and 25 // decode these, since those clip to 32 bits, which we might in rare 26 // cases want to overflow. A 64-bit float can represent 48-bit 27 // integers precisely. 28 29 const lower16 = 0xffff 30 const factor16 = Math.pow(2, 16) 31 32 function makeRecover(index: number, offset: number) { return index + offset * factor16 } 33 function recoverIndex(value: number) { return value & lower16 } 34 function recoverOffset(value: number) { return (value - (value & lower16)) / factor16 } 35 36 const DEL_BEFORE = 1, DEL_AFTER = 2, DEL_ACROSS = 4, DEL_SIDE = 8 37 38 /// An object representing a mapped position with extra 39 /// information. 40 export class MapResult { 41 /// @internal 42 constructor( 43 /// The mapped version of the position. 44 readonly pos: number, 45 /// @internal 46 readonly delInfo: number, 47 /// @internal 48 readonly recover: number | null 49 ) {} 50 51 /// Tells you whether the position was deleted, that is, whether the 52 /// step removed the token on the side queried (via the `assoc`) 53 /// argument from the document. 54 get deleted() { return (this.delInfo & DEL_SIDE) > 0 } 55 56 /// Tells you whether the token before the mapped position was deleted. 57 get deletedBefore() { return (this.delInfo & (DEL_BEFORE | DEL_ACROSS)) > 0 } 58 59 /// True when the token after the mapped position was deleted. 60 get deletedAfter() { return (this.delInfo & (DEL_AFTER | DEL_ACROSS)) > 0 } 61 62 /// Tells whether any of the steps mapped through deletes across the 63 /// position (including both the token before and after the 64 /// position). 65 get deletedAcross() { return (this.delInfo & DEL_ACROSS) > 0 } 66 } 67 68 /// A map describing the deletions and insertions made by a step, which 69 /// can be used to find the correspondence between positions in the 70 /// pre-step version of a document and the same position in the 71 /// post-step version. 72 export class StepMap implements Mappable { 73 /// Create a position map. The modifications to the document are 74 /// represented as an array of numbers, in which each group of three 75 /// represents a modified chunk as `[start, oldSize, newSize]`. 76 constructor( 77 /// @internal 78 readonly ranges: readonly number[], 79 /// @internal 80 readonly inverted = false 81 ) { 82 if (!ranges.length && StepMap.empty) return StepMap.empty 83 } 84 85 /// @internal 86 recover(value: number) { 87 let diff = 0, index = recoverIndex(value) 88 if (!this.inverted) for (let i = 0; i < index; i++) 89 diff += this.ranges[i * 3 + 2] - this.ranges[i * 3 + 1] 90 return this.ranges[index * 3] + diff + recoverOffset(value) 91 } 92 93 mapResult(pos: number, assoc = 1): MapResult { return this._map(pos, assoc, false) as MapResult } 94 95 map(pos: number, assoc = 1): number { return this._map(pos, assoc, true) as number } 96 97 /// @internal 98 _map(pos: number, assoc: number, simple: boolean) { 99 let diff = 0, oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2 100 for (let i = 0; i < this.ranges.length; i += 3) { 101 let start = this.ranges[i] - (this.inverted ? diff : 0) 102 if (start > pos) break 103 let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex], end = start + oldSize 104 if (pos <= end) { 105 let side = !oldSize ? assoc : pos == start ? -1 : pos == end ? 1 : assoc 106 let result = start + diff + (side < 0 ? 0 : newSize) 107 if (simple) return result 108 let recover = pos == (assoc < 0 ? start : end) ? null : makeRecover(i / 3, pos - start) 109 let del = pos == start ? DEL_AFTER : pos == end ? DEL_BEFORE : DEL_ACROSS 110 if (assoc < 0 ? pos != start : pos != end) del |= DEL_SIDE 111 return new MapResult(result, del, recover) 112 } 113 diff += newSize - oldSize 114 } 115 return simple ? pos + diff : new MapResult(pos + diff, 0, null) 116 } 117 118 /// @internal 119 touches(pos: number, recover: number) { 120 let diff = 0, index = recoverIndex(recover) 121 let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2 122 for (let i = 0; i < this.ranges.length; i += 3) { 123 let start = this.ranges[i] - (this.inverted ? diff : 0) 124 if (start > pos) break 125 let oldSize = this.ranges[i + oldIndex], end = start + oldSize 126 if (pos <= end && i == index * 3) return true 127 diff += this.ranges[i + newIndex] - oldSize 128 } 129 return false 130 } 131 132 /// Calls the given function on each of the changed ranges included in 133 /// this map. 134 forEach(f: (oldStart: number, oldEnd: number, newStart: number, newEnd: number) => void) { 135 let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2 136 for (let i = 0, diff = 0; i < this.ranges.length; i += 3) { 137 let start = this.ranges[i], oldStart = start - (this.inverted ? diff : 0), newStart = start + (this.inverted ? 0 : diff) 138 let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex] 139 f(oldStart, oldStart + oldSize, newStart, newStart + newSize) 140 diff += newSize - oldSize 141 } 142 } 143 144 /// Create an inverted version of this map. The result can be used to 145 /// map positions in the post-step document to the pre-step document. 146 invert() { 147 return new StepMap(this.ranges, !this.inverted) 148 } 149 150 /// @internal 151 toString() { 152 return (this.inverted ? "-" : "") + JSON.stringify(this.ranges) 153 } 154 155 /// Create a map that moves all positions by offset `n` (which may be 156 /// negative). This can be useful when applying steps meant for a 157 /// sub-document to a larger document, or vice-versa. 158 static offset(n: number) { 159 return n == 0 ? StepMap.empty : new StepMap(n < 0 ? [0, -n, 0] : [0, 0, n]) 160 } 161 162 /// A StepMap that contains no changed ranges. 163 static empty = new StepMap([]) 164 } 165 166 /// A mapping represents a pipeline of zero or more [step 167 /// maps](#transform.StepMap). It has special provisions for losslessly 168 /// handling mapping positions through a series of steps in which some 169 /// steps are inverted versions of earlier steps. (This comes up when 170 /// ‘[rebasing](/docs/guide/#transform.rebasing)’ steps for 171 /// collaboration or history management.) 172 export class Mapping implements Mappable { 173 /// Create a new mapping with the given position maps. 174 constructor( 175 maps?: readonly StepMap[], 176 /// @internal 177 public mirror?: number[], 178 /// The starting position in the `maps` array, used when `map` or 179 /// `mapResult` is called. 180 public from = 0, 181 /// The end position in the `maps` array. 182 public to = maps ? maps.length : 0 183 ) { 184 this._maps = (maps as StepMap[]) || [] 185 this.ownData = !(maps || mirror) 186 } 187 188 /// The step maps in this mapping. 189 get maps(): readonly StepMap[] { return this._maps } 190 191 private _maps: StepMap[] 192 // False if maps/mirror are shared arrays that we shouldn't mutate 193 private ownData: boolean 194 195 /// Create a mapping that maps only through a part of this one. 196 slice(from = 0, to = this.maps.length) { 197 return new Mapping(this._maps, this.mirror, from, to) 198 } 199 200 /// Add a step map to the end of this mapping. If `mirrors` is 201 /// given, it should be the index of the step map that is the mirror 202 /// image of this one. 203 appendMap(map: StepMap, mirrors?: number) { 204 if (!this.ownData) { 205 this._maps = this._maps.slice() 206 this.mirror = this.mirror && this.mirror.slice() 207 this.ownData = true 208 } 209 this.to = this._maps.push(map) 210 if (mirrors != null) this.setMirror(this._maps.length - 1, mirrors) 211 } 212 213 /// Add all the step maps in a given mapping to this one (preserving 214 /// mirroring information). 215 appendMapping(mapping: Mapping) { 216 for (let i = 0, startSize = this._maps.length; i < mapping._maps.length; i++) { 217 let mirr = mapping.getMirror(i) 218 this.appendMap(mapping._maps[i], mirr != null && mirr < i ? startSize + mirr : undefined) 219 } 220 } 221 222 /// Finds the offset of the step map that mirrors the map at the 223 /// given offset, in this mapping (as per the second argument to 224 /// `appendMap`). 225 getMirror(n: number): number | undefined { 226 if (this.mirror) for (let i = 0; i < this.mirror.length; i++) 227 if (this.mirror[i] == n) return this.mirror[i + (i % 2 ? -1 : 1)] 228 } 229 230 /// @internal 231 setMirror(n: number, m: number) { 232 if (!this.mirror) this.mirror = [] 233 this.mirror.push(n, m) 234 } 235 236 /// Append the inverse of the given mapping to this one. 237 appendMappingInverted(mapping: Mapping) { 238 for (let i = mapping.maps.length - 1, totalSize = this._maps.length + mapping._maps.length; i >= 0; i--) { 239 let mirr = mapping.getMirror(i) 240 this.appendMap(mapping._maps[i].invert(), mirr != null && mirr > i ? totalSize - mirr - 1 : undefined) 241 } 242 } 243 244 /// Create an inverted version of this mapping. 245 invert() { 246 let inverse = new Mapping 247 inverse.appendMappingInverted(this) 248 return inverse 249 } 250 251 /// Map a position through this mapping. 252 map(pos: number, assoc = 1) { 253 if (this.mirror) return this._map(pos, assoc, true) as number 254 for (let i = this.from; i < this.to; i++) 255 pos = this._maps[i].map(pos, assoc) 256 return pos 257 } 258 259 /// Map a position through this mapping, returning a mapping 260 /// result. 261 mapResult(pos: number, assoc = 1) { return this._map(pos, assoc, false) as MapResult } 262 263 /// @internal 264 _map(pos: number, assoc: number, simple: boolean) { 265 let delInfo = 0 266 267 for (let i = this.from; i < this.to; i++) { 268 let map = this._maps[i], result = map.mapResult(pos, assoc) 269 if (result.recover != null) { 270 let corr = this.getMirror(i) 271 if (corr != null && corr > i && corr < this.to) { 272 i = corr 273 pos = this._maps[corr].recover(result.recover) 274 continue 275 } 276 } 277 278 delInfo |= result.delInfo 279 pos = result.pos 280 } 281 282 return simple ? pos : new MapResult(pos, delInfo, null) 283 } 284 }