{"record":{"id":"2bc4e412ff8233b3","repo":"zed-industries/zed","slug":"byte-index-is-out-of-bounds-of-rope-length","errorCode":null,"errorMessage":"byte index {} is out of bounds of rope (length: {})","messagePattern":"byte index (.+?) is out of bounds of rope \\(length: (.+?)\\)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/rope/src/rope.rs","lineNumber":65,"sourceCode":"        let chunk_offset = offset - start;\n        item.map(|chunk| chunk.is_char_boundary(chunk_offset))\n            .unwrap_or(false)\n    }\n\n    #[track_caller]\n    #[inline(always)]\n    pub fn assert_char_boundary<const PANIC: bool>(&self, offset: usize) -> bool {\n        if self.chunks.is_empty() && offset == 0 {\n            return true;\n        }\n        let (start, _, item) = self.chunks.find::<usize, _>((), &offset, Bias::Left);\n        match item {\n            Some(chunk) => {\n                let chunk_offset = offset - start;\n                chunk.assert_char_boundary::<PANIC>(chunk_offset)\n            }\n            None if PANIC => {\n                panic!(\n                    \"byte index {} is out of bounds of rope (length: {})\",\n                    offset,\n                    self.len()\n                );\n            }\n            None => {\n                log::error!(\n                    \"byte index {} is out of bounds of rope (length: {})\",\n                    offset,\n                    self.len()\n                );\n                false\n            }\n        }\n    }\n\n    pub fn floor_char_boundary(&self, index: usize) -> usize {\n        if index >= self.len() {","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/zed-industries/zed/blob/f4178619acd0d47ea1f76a2025c42962c6d6638c/crates/rope/src/rope.rs#L47-L83","documentation":"Zed's Rope is the text data structure behind every editor buffer, stored as a sum tree of chunks. assert_char_boundary resolves a byte offset by finding the chunk that contains it; when no chunk matches and the call was made with PANIC=true, the rope aborts with the offending offset and its real length. Nearly every offset-taking rope API funnels through this check, so the panic always means a caller computed an offset against different text than the rope actually contains.","triggerScenarios":"Calling offset-based APIs with an offset greater than rope.len(): rope.offset_to_point(offset), rope.offset_to_point_utf16, range slicing, or buffer edits applying a range captured from an older snapshot. The classic producer is an async task that captures an offset, awaits, then applies it after an edit shortened the buffer.","commonSituations":"Features that snapshot cursor/selection offsets and reuse them after an await raced a buffer edit (format-on-save, lint fixes, collaboration); off-by-one from adding to an offset; using the len() of a substring instead of the rope; tests that build a small rope and index past its end.","solutions":["Clamp the offset before use: offset = offset.min(rope.len()), and snap to a character boundary with rope.clip_offset(offset, Bias::Left) (crates/rope/src/rope.rs:536).","Recompute offsets from the current buffer snapshot after any await or edit instead of reusing values captured earlier.","When slicing, clamp both ends of the range to 0..rope.len() before calling range APIs.","Use the offset and rope length printed in the panic message plus the backtrace (the wrapper, e.g. offset_to_point) to find which call site passed the stale value."],"exampleFix":"// before\nlet point = rope.offset_to_point(offset); // offset captured before an edit\n\n// after\nuse sum_tree::Bias;\nlet offset = rope.clip_offset(offset.min(rope.len()), Bias::Left);\nlet point = rope.offset_to_point(offset);","handlingStrategy":"validation","validationCode":"use sum_tree::Bias;\n\nfn clamp_to_rope(rope: &Rope, offset: usize) -> usize {\n    rope.clip_offset(offset.min(rope.len()), Bias::Left)\n}\n\n// non-panicking probe of the same check the panic path performs:\nfn is_valid_offset(rope: &Rope, offset: usize) -> bool {\n    rope.assert_char_boundary::<false>(offset)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never reuse a numeric offset across an await point; re-derive it from the live snapshot.","Clamp externally computed offsets (LSP positions, search results from other threads) with clip_offset before passing them in.","Store snapshots rather than bare offsets when mixing coordinates from different sources.","Unit-test edge cases: empty rope, offset == len, offset == len + 1."],"tags":["rope","text-buffer","offset","out-of-bounds","panic","zed"],"backgroundTag":"index-out-of-bounds","analyzedSha":"f4178619acd0d47ea1f76a2025c42962c6d6638c","analyzedAt":"2026-08-20T19:29:52.058Z","contentChangedAt":"2026-08-20T19:29:52.058Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}