tracked-deletes.ts 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. import { TrackedChangeList } from 'overleaf-editor-core'
  2. import { EditorState } from '@codemirror/state'
  3. import { rangesState } from '@/features/source-editor/extensions/history-ot'
  4. export const trackedDeletesFromState = (state: EditorState) =>
  5. new TrackedDeletes(state.field(rangesState).trackedChanges)
  6. type OffsetTable = { pos: number; map: (pos: number) => number }[]
  7. export class TrackedDeletes {
  8. private offsets: {
  9. toCM6: OffsetTable
  10. toSnapshot: OffsetTable
  11. }
  12. constructor(trackedChanges: TrackedChangeList) {
  13. this.offsets = {
  14. toCM6: [{ pos: 0, map: pos => pos }],
  15. toSnapshot: [{ pos: 0, map: pos => pos }],
  16. }
  17. // Offset of the snapshot pos relative to the CM6 pos
  18. let offset = 0
  19. for (const change of trackedChanges.asSorted()) {
  20. if (change.tracking.type === 'delete') {
  21. const deleteLength = change.range.length
  22. const deletePos = change.range.pos
  23. const oldOffset = offset
  24. const newOffset = offset + deleteLength
  25. this.offsets.toSnapshot.push({
  26. pos: change.range.pos - offset + 1,
  27. map: pos => pos + newOffset,
  28. })
  29. this.offsets.toCM6.push({
  30. pos: change.range.pos,
  31. map: () => deletePos - oldOffset,
  32. })
  33. this.offsets.toCM6.push({
  34. pos: change.range.pos + deleteLength,
  35. map: pos => pos - newOffset,
  36. })
  37. offset = newOffset
  38. }
  39. }
  40. }
  41. toCodeMirror(snapshotPos: number) {
  42. return this.mapPos(snapshotPos, this.offsets.toCM6)
  43. }
  44. toSnapshot(cm6Pos: number) {
  45. return this.mapPos(cm6Pos, this.offsets.toSnapshot)
  46. }
  47. mapPos(pos: number, offsets: OffsetTable) {
  48. // Binary search for the offset at the last position before pos
  49. let low = 0
  50. let high = offsets.length - 1
  51. while (low < high) {
  52. const middle = Math.ceil((low + high) / 2)
  53. const entry = offsets[middle]
  54. if (entry.pos < pos) {
  55. // This entry could be the right offset, but lower entries are too low
  56. // Because we used Math.ceil(), middle is higher than low and the
  57. // algorithm progresses.
  58. low = middle
  59. } else if (entry.pos > pos) {
  60. // This entry is too high
  61. high = middle - 1
  62. } else {
  63. // This is the right entry
  64. return entry.map(pos)
  65. }
  66. }
  67. return offsets[low].map(pos)
  68. }
  69. }