Fix panic when folding immediately after a tab (#49920)

fixes ZED-57B

Before you mark this PR as ready for review, make sure that you have:
- [ ] Added a solid test coverage and/or screenshots from doing manual
testing
- [ ] Done a self-review taking into account security and performance
aspects
- [ ] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- Fixed a (rare) panic when a fold was created immediately following a
tab character
This commit is contained in:
Conrad Irwin authored and GitHub committed 2026-02-23 13:16:33 -07:00
1 parent 3333c79da1
commit 9e87fefe3f
1 file changed
+49 -14
+49 -14
View File
@@ -604,19 +604,16 @@ impl<'a> Iterator for TabChunks<'a> {
#[ztracing::instrument(skip_all)]
fn next(&mut self) -> Option<Self::Item> {
if self.chunk.text.is_empty() {
if let Some(chunk) = self.fold_chunks.next() {
self.chunk = chunk;
if self.inside_leading_tab {
self.chunk.text = &self.chunk.text[1..];
self.chunk.tabs >>= 1;
self.chunk.chars >>= 1;
self.chunk.newlines >>= 1;
self.inside_leading_tab = false;
self.input_column += 1;
}
} else {
return None;
while self.chunk.text.is_empty() {
let chunk = self.fold_chunks.next()?;
self.chunk = chunk;
if self.inside_leading_tab {
self.chunk.text = &self.chunk.text[1..];
self.chunk.tabs >>= 1;
self.chunk.chars >>= 1;
self.chunk.newlines >>= 1;
self.inside_leading_tab = false;
self.input_column += 1;
}
}
@@ -710,7 +707,7 @@ mod tests {
use crate::{
MultiBuffer,
display_map::{
fold_map::{FoldMap, FoldOffset},
fold_map::{FoldMap, FoldOffset, FoldPlaceholder},
inlay_map::InlayMap,
},
};
@@ -1090,6 +1087,44 @@ mod tests {
}
}
#[gpui::test]
fn test_empty_chunk_after_leading_tab_trim(cx: &mut gpui::App) {
// We fold "hello" (offsets 1..6) so the fold map creates a
// transform boundary at offset 1, producing a 1-byte fold chunk
// for the tab.
let text = "\thello";
let buffer = MultiBuffer::build_simple(text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let mut fold_map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = fold_map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![(
MultiBufferOffset(1)..MultiBufferOffset(6),
FoldPlaceholder::test(),
)]);
let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
let tab_size = NonZeroU32::new(4).unwrap();
let (_, tab_snapshot) = TabMap::new(fold_snapshot, tab_size);
// The tab at column 0 expands to 4 spaces (columns 0‥4).
// Seek starting at column 2 (middle of that tab) so that
// `inside_leading_tab = true` and `to_next_stop = 2`.
// Set the end just past the tab expansion so the iterator must
// process the tab byte from the fold chunk.
let max = tab_snapshot.max_point();
let start = TabPoint::new(0, 2);
let end = max;
// This should not panic.
let result: String = tab_snapshot
.chunks(start..end, false, Highlights::default())
.map(|c| c.text)
.collect();
assert!(!result.is_empty());
}
#[gpui::test(iterations = 100)]
fn test_random_tabs(cx: &mut gpui::App, mut rng: StdRng) {
let tab_size = NonZeroU32::new(rng.random_range(1..=4)).unwrap();