mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 14:43:46 +01:00
Fix History OT base length when a tracked delete is in the document GitOrigin-RevId: 414349aedd498c2635bb3067388dcc7291e32ee6
92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
import { TrackedChangeList } from 'overleaf-editor-core'
|
|
import { EditorState } from '@codemirror/state'
|
|
import { rangesState } from '@/features/source-editor/extensions/history-ot'
|
|
|
|
export const trackedDeletesFromState = (state: EditorState) =>
|
|
new TrackedDeletes(state.field(rangesState).trackedChanges)
|
|
|
|
type OffsetTable = { pos: number; map: (pos: number) => number }[]
|
|
|
|
export class TrackedDeletes {
|
|
private offsets: {
|
|
toCM6: OffsetTable
|
|
toSnapshot: OffsetTable
|
|
}
|
|
|
|
/**
|
|
* Total length of tracked deletes, i.e. the difference between the snapshot
|
|
* length and the visible CodeMirror length.
|
|
*/
|
|
readonly totalLength: number
|
|
|
|
constructor(trackedChanges: TrackedChangeList) {
|
|
this.offsets = {
|
|
toCM6: [{ pos: 0, map: pos => pos }],
|
|
toSnapshot: [{ pos: 0, map: pos => pos }],
|
|
}
|
|
|
|
// Offset of the snapshot pos relative to the CM6 pos
|
|
let offset = 0
|
|
for (const change of trackedChanges.asSorted()) {
|
|
if (change.tracking.type === 'delete') {
|
|
const deleteLength = change.range.length
|
|
const deletePos = change.range.pos
|
|
const oldOffset = offset
|
|
const newOffset = offset + deleteLength
|
|
this.offsets.toSnapshot.push({
|
|
pos: change.range.pos - offset + 1,
|
|
map: pos => pos + newOffset,
|
|
})
|
|
this.offsets.toCM6.push({
|
|
pos: change.range.pos,
|
|
map: () => deletePos - oldOffset,
|
|
})
|
|
this.offsets.toCM6.push({
|
|
pos: change.range.pos + deleteLength,
|
|
map: pos => pos - newOffset,
|
|
})
|
|
offset = newOffset
|
|
}
|
|
}
|
|
this.totalLength = offset
|
|
}
|
|
|
|
toCodeMirror(snapshotPos: number) {
|
|
return this.mapPos(snapshotPos, this.offsets.toCM6)
|
|
}
|
|
|
|
/**
|
|
* Map a CodeMirror position to a snapshot position, for placing an edit. At a
|
|
* tracked-delete boundary this deliberately maps to *before* the delete (so
|
|
* typing lands before the struck-through text), which means it under-counts
|
|
* for a delete at the end of the document. Use `totalLength` for the
|
|
* whole-document length, not `toSnapshot(docLength)`.
|
|
*/
|
|
toSnapshot(cm6Pos: number) {
|
|
return this.mapPos(cm6Pos, this.offsets.toSnapshot)
|
|
}
|
|
|
|
mapPos(pos: number, offsets: OffsetTable) {
|
|
// Binary search for the offset at the last position before pos
|
|
let low = 0
|
|
let high = offsets.length - 1
|
|
while (low < high) {
|
|
const middle = Math.ceil((low + high) / 2)
|
|
const entry = offsets[middle]
|
|
if (entry.pos < pos) {
|
|
// This entry could be the right offset, but lower entries are too low
|
|
// Because we used Math.ceil(), middle is higher than low and the
|
|
// algorithm progresses.
|
|
low = middle
|
|
} else if (entry.pos > pos) {
|
|
// This entry is too high
|
|
high = middle - 1
|
|
} else {
|
|
// This is the right entry
|
|
return entry.map(pos)
|
|
}
|
|
}
|
|
return offsets[low].map(pos)
|
|
}
|
|
}
|