line-tracker.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import OError from '@overleaf/o-error'
  2. import { Text } from '@codemirror/state'
  3. import { Word } from './spellchecker'
  4. import { ViewUpdate } from '@codemirror/view'
  5. export class LineTracker {
  6. private _lines: boolean[]
  7. constructor(doc: Text) {
  8. /*
  9. * Maintain an array of booleans, one for each line of the document.
  10. * `true` means a line has changed
  11. */
  12. this._lines = new Array(doc.lines).fill(true)
  13. }
  14. dump() {
  15. return [...this._lines]
  16. }
  17. count() {
  18. return this._lines.length
  19. }
  20. lineHasChanged(lineNumber: number) {
  21. return this._lines[lineNumber - 1] === true
  22. }
  23. /*
  24. * Given a list of words, clear the 'changed' mark
  25. * on the lines the words are on
  26. */
  27. clearChangedLinesForWords(words: Word[]) {
  28. words.forEach(word => {
  29. this.clearLine(word.lineNumber)
  30. })
  31. }
  32. clearLine(lineNumber: number) {
  33. this._lines[lineNumber - 1] = false
  34. }
  35. clearAllLines() {
  36. this._lines = this._lines.map(() => false)
  37. }
  38. resetAllLines() {
  39. this._lines = this._lines.map(() => true)
  40. }
  41. markLineAsUpdated(lineNumber: number) {
  42. this._lines[lineNumber - 1] = true
  43. }
  44. /*
  45. * On update, for all changes, mark the affected lines
  46. * as changed
  47. */
  48. applyUpdate(update: ViewUpdate) {
  49. for (const transaction of update.transactions) {
  50. if (transaction.docChanged) {
  51. let positionShift = 0
  52. transaction.changes.iterChanges(
  53. (fromA, toA, fromB, toB, insertedText) => {
  54. const insertedLength = insertedText.length
  55. const removedLength = toA - fromA
  56. const hasInserted = insertedLength > 0
  57. const hasRemoved = removedLength > 0
  58. const doc = update.state.doc
  59. const oldDoc = transaction.startState.doc
  60. if (hasRemoved) {
  61. const startLine = oldDoc.lineAt(fromA).number
  62. const endLine = oldDoc.lineAt(toA).number
  63. /* Mark start line as changed, and remove deleted lines
  64. * Example:
  65. * with this text:
  66. * |1|aaaa|
  67. * |2|bbbb| => [false, false, false, false]
  68. * |3|cccc|
  69. * |4|dddd|
  70. *
  71. * with a selection covering 'bbcccc' across lines 2 and 3,
  72. * press backspace,
  73. * resulting in:
  74. * |1|aaaa|
  75. * |2|bb| => [false, true, false]
  76. * |3|dddd|
  77. */
  78. this._lines.splice(startLine - 1, endLine - startLine + 1, true)
  79. }
  80. if (hasInserted) {
  81. const startLine = doc.lineAt(fromB).number
  82. /* Mark start line as changed, and insert new (changed) lines after.
  83. * Example:
  84. * with this text:
  85. * |1|aaaa|
  86. * |2|bbbb| => [false, false, false]
  87. * |3|cccc|
  88. *
  89. * with the cursor at the end of line 2,
  90. * insert the following text:
  91. * |1|xx|
  92. * |2|yy|
  93. *
  94. * results in:
  95. * |1|aaaa|
  96. * |2|bbbbxx| => [false, true, true, false]
  97. * |3|yy|
  98. * |4|cccc|
  99. */
  100. const changes = new Array(insertedText.lines).fill(true)
  101. this._lines.splice(startLine - 1, 1, ...changes)
  102. }
  103. positionShift = positionShift - removedLength + insertedLength
  104. }
  105. )
  106. if (update.state.doc.lines !== this._lines.length) {
  107. throw new OError(
  108. 'LineTracker length does not match document line count'
  109. ).withInfo({
  110. documentLines: update.state.doc.lines,
  111. trackerLines: this._lines.length,
  112. })
  113. }
  114. }
  115. }
  116. }
  117. }