Mapping
Position tracking across document transforms.
A selection anchored at pos(12) means one thing before a transaction and something different after. If the transaction inserts five UTF-16 units at pos(8), leaving the anchor at the raw number 12 would point into different content. The original anchor must move to pos(17).
A Mapping describes how every old position lands in the new document. It is produced by apply, consumed by selection preservation and by any plugin state that references positions.
Worked example
const mapping = MappingImpl.replacement({
start: pos(8),
end: pos(8),
inserted: 5,
});
const oldPos: Pos = pos(12);
const newPos: Pos = mapping.mapPos(oldPos, 1);
Here newPos is pos(17): the old position moved five UTF-16 units to the right.
Mapping also handles deleted ranges:
const deletion = MappingImpl.replacement({
start: pos(10),
end: pos(14),
inserted: 0,
});
const insideDeletedRange = deletion.mapPos(pos(12), 1);
insideDeletedRange lands at the collapsed deletion boundary, pos(10).
const result = applyTransaction(doc, transaction);
const newSelection = mapSelection(oldTextSelection, (p, assoc) =>
result.mapping.mapPos(p, assoc),
);
Assoc
Assoc = -1 | 1. When an old position falls exactly on a change boundary — say the position is at the insertion point — the mapping has two reasonable answers. Assoc = -1 says "stay on the left side, the text that came before." Assoc = 1 says "move to the right, after the inserted content." Typing text biases +1 so the caret ends up after the new character; a user selection that ends at the change boundary may want -1 to keep its tail from growing.
Where mapping is used
- Selection preservation.
mapSelection(selection, mapPos)maps aTextSelectionthrough a transform. Structural selections are path-based; the engine carries or repairs them through the supported structural domains and otherwise collapses them to a deterministic text boundary. - Step remapping.
step.map(mapping)returns a rewritten step whose positions move through the transform, ornullwhen the step no longer applies. - Plugin state. A plugin that stores a
Posmust map it through every commit, or it will drift.
API
interface Mapping {
mapPos(pos: Pos, assoc?: Assoc): Pos;
mapRange(range: Range): Range;
append(other: Mapping): Mapping;
}
mapSelection(selection: Selection, mapPos: (p: Pos, a: Assoc) => Pos): Selection;
The engine does not expose position mapping through the public DispatchResult by accident — result.mapping is deliberately there so subscribers and plugins can remap their own state alongside the committed doc.