{"record":{"id":"0d359e739fb04e0a","repo":"overleaf/overleaf","slug":"tracked-changes-range-cannot-be-empty","errorCode":null,"errorMessage":"Tracked changes range cannot be empty","messagePattern":"Tracked changes range cannot be empty","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"libraries/overleaf-editor-core/lib/file_data/tracked_change_list.js","lineNumber":135,"sourceCode":"   *\n   * @private\n   * @returns {void}\n   */\n  _mergeRanges() {\n    if (this._trackedChanges.length < 2) {\n      return\n    }\n    // ranges are non-overlapping so we can sort based on their first indices\n    this._trackedChanges.sort((a, b) => a.range.start - b.range.start)\n    const newTrackedChanges = [this._trackedChanges[0]]\n    for (let i = 1; i < this._trackedChanges.length; i++) {\n      const last = newTrackedChanges[newTrackedChanges.length - 1]\n      const current = this._trackedChanges[i]\n      if (last.range.overlaps(current.range)) {\n        throw new Error('Ranges cannot overlap')\n      }\n      if (current.range.isEmpty()) {\n        throw new Error('Tracked changes range cannot be empty')\n      }\n      if (last.canMerge(current)) {\n        newTrackedChanges[newTrackedChanges.length - 1] = last.merge(current)\n      } else {\n        newTrackedChanges.push(current)\n      }\n    }\n    this._trackedChanges = newTrackedChanges\n  }\n\n  /**\n   * Apply an insert operation\n   *\n   * @param {number} cursor\n   * @param {string} insertedText\n   * @param {{tracking?: TrackingProps}} opts\n   */\n  applyInsert(cursor, insertedText, opts = {}) {","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/overleaf/overleaf/blob/28ad3b03b71cb4311decdcb55c36b33ec10d72db/libraries/overleaf-editor-core/lib/file_data/tracked_change_list.js#L117-L153","documentation":"During the same _mergeRanges normalization pass, after confirming no overlaps, the list asserts that every tracked change covers a non-empty range. An empty range (start === end) is meaningless for a tracked change, so the list refuses it and throws.","triggerScenarios":"Adding a TrackedChange whose Range has start === end, or an operation (applyInsert/applyDelete/applyRetain/applyTextOperation/add) that shrinks a tracked change to zero length, causing _mergeRanges to encounter an empty range.","commonSituations":"Deleting the entire span covered by a tracked change and reusing the stale (now empty) range; constructing ranges from computed offsets where start and end coincide after a delete; off-by-one offset arithmetic.","solutions":["Skip creating TrackedChange entries whose range isEmpty() before adding them.","After deletes, recompute or remove tracked changes whose spans were fully deleted.","Validate offsets (range.start < range.end) when building ranges programmatically.","Filter out empty ranges from any deserialized/persisted tracked-change data before loading."],"exampleFix":"// before\nlist.add(new TrackedChange(new Range(pos, pos), tracking))\n// after\nconst range = new Range(start, end)\nif (!range.isEmpty()) { list.add(new TrackedChange(range, tracking)) }","handlingStrategy":"validation","validationCode":"// drop empty ranges before adding\nconst usable = candidate.filter(tc => !tc.range.isEmpty())","typeGuard":null,"tryCatchPattern":"try {\n  list.add(tc)\n} catch (err) {\n  if (err.message !== 'Tracked changes range cannot be empty') throw err\n  // skip: the change spanned only deleted content\n}","preventionTips":["Assert range.start < range.end when constructing ranges","Remove tracked changes whose entire span was deleted instead of keeping a zero-length range","Filter empty ranges when importing persisted tracked-change data"],"tags":["tracked-changes","empty-range","invariant"],"backgroundTag":"empty-range-tracked-change","analyzedSha":"28ad3b03b71cb4311decdcb55c36b33ec10d72db","analyzedAt":"2026-09-03T02:10:22.807Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}