New excerpt controls (#24428)

Release Notes:

- Multibuffers now use less vertical space for excerpt boundaries.
Additionally the expand up/down arrows are hidden at the start and end
of the buffers

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Zed AI <claude-3.5-sonnet@zed.dev>
This commit is contained in:
Conrad Irwin 2025-03-13 15:52:47 -06:00 committed by GitHub
parent 3935e8343a
commit e3c0f56a96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 513 additions and 707 deletions

View file

@ -37,10 +37,8 @@ pub struct BlockMap {
custom_blocks: Vec<Arc<CustomBlock>>,
custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
transforms: RefCell<SumTree<Transform>>,
show_excerpt_controls: bool,
buffer_header_height: u32,
excerpt_header_height: u32,
excerpt_footer_height: u32,
pub(super) folded_buffers: HashSet<BufferId>,
}
@ -58,7 +56,6 @@ pub struct BlockSnapshot {
custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
pub(super) buffer_header_height: u32,
pub(super) excerpt_header_height: u32,
pub(super) excerpt_footer_height: u32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
@ -285,14 +282,12 @@ pub enum Block {
first_excerpt: ExcerptInfo,
prev_excerpt: Option<ExcerptInfo>,
height: u32,
show_excerpt_controls: bool,
},
ExcerptBoundary {
prev_excerpt: Option<ExcerptInfo>,
next_excerpt: Option<ExcerptInfo>,
height: u32,
starts_new_buffer: bool,
show_excerpt_controls: bool,
},
}
@ -362,13 +357,11 @@ impl Debug for Block {
first_excerpt,
prev_excerpt,
height,
show_excerpt_controls,
} => f
.debug_struct("FoldedBuffer")
.field("first_excerpt", &first_excerpt)
.field("prev_excerpt", prev_excerpt)
.field("height", height)
.field("show_excerpt_controls", show_excerpt_controls)
.finish(),
Self::ExcerptBoundary {
starts_new_buffer,
@ -413,10 +406,8 @@ pub struct BlockRows<'a> {
impl BlockMap {
pub fn new(
wrap_snapshot: WrapSnapshot,
show_excerpt_controls: bool,
buffer_header_height: u32,
excerpt_header_height: u32,
excerpt_footer_height: u32,
) -> Self {
let row_count = wrap_snapshot.max_point().row() + 1;
let mut transforms = SumTree::default();
@ -428,10 +419,8 @@ impl BlockMap {
folded_buffers: HashSet::default(),
transforms: RefCell::new(transforms),
wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
show_excerpt_controls,
buffer_header_height,
excerpt_header_height,
excerpt_footer_height,
};
map.sync(
&wrap_snapshot,
@ -454,7 +443,6 @@ impl BlockMap {
custom_blocks_by_id: self.custom_blocks_by_id.clone(),
buffer_header_height: self.buffer_header_height,
excerpt_header_height: self.excerpt_header_height,
excerpt_footer_height: self.excerpt_footer_height,
},
}
}
@ -650,8 +638,6 @@ impl BlockMap {
if buffer.show_headers() {
blocks_in_edit.extend(BlockMap::header_and_footer_blocks(
self.show_excerpt_controls,
self.excerpt_footer_height,
self.buffer_header_height,
self.excerpt_header_height,
buffer,
@ -722,13 +708,7 @@ impl BlockMap {
}
}
pub fn show_excerpt_controls(&self) -> bool {
self.show_excerpt_controls
}
fn header_and_footer_blocks<'a, R, T>(
show_excerpt_controls: bool,
excerpt_footer_height: u32,
buffer_header_height: u32,
excerpt_header_height: u32,
buffer: &'a multi_buffer::MultiBufferSnapshot,
@ -774,11 +754,6 @@ impl BlockMap {
.filter(|prev| !folded_buffers.contains(&prev.buffer_id));
let mut height = 0;
if prev_excerpt.is_some() {
if show_excerpt_controls {
height += excerpt_footer_height;
}
}
if let Some(new_buffer_id) = new_buffer_id {
let first_excerpt = excerpt_boundary.next.clone().unwrap();
@ -812,7 +787,6 @@ impl BlockMap {
Block::FoldedBuffer {
prev_excerpt,
height: height + buffer_header_height,
show_excerpt_controls,
first_excerpt,
},
));
@ -822,9 +796,6 @@ impl BlockMap {
if excerpt_boundary.next.is_some() {
if new_buffer_id.is_some() {
height += buffer_header_height;
if show_excerpt_controls {
height += excerpt_header_height;
}
} else {
height += excerpt_header_height;
}
@ -845,7 +816,6 @@ impl BlockMap {
next_excerpt: excerpt_boundary.next,
height,
starts_new_buffer: new_buffer_id.is_some(),
show_excerpt_controls,
},
))
})
@ -1432,7 +1402,8 @@ impl BlockSnapshot {
})
}
pub fn sticky_header_excerpt(&self, top_row: u32) -> Option<StickyHeaderExcerpt<'_>> {
pub fn sticky_header_excerpt(&self, position: f32) -> Option<StickyHeaderExcerpt<'_>> {
let top_row = position as u32;
let mut cursor = self.transforms.cursor::<BlockRow>(&());
cursor.seek(&BlockRow(top_row), Bias::Left, &());
@ -1445,19 +1416,13 @@ impl BlockSnapshot {
prev_excerpt,
next_excerpt,
starts_new_buffer,
show_excerpt_controls,
..
}) => {
let matches_start = if *show_excerpt_controls && prev_excerpt.is_some() {
start < top_row
} else {
start <= top_row
};
let matches_start = (start as f32) < position;
if matches_start && top_row <= end {
return next_excerpt.as_ref().map(|excerpt| StickyHeaderExcerpt {
next_buffer_row: None,
next_excerpt_controls_present: *show_excerpt_controls,
excerpt,
});
}
@ -1467,7 +1432,6 @@ impl BlockSnapshot {
return prev_excerpt.as_ref().map(|excerpt| StickyHeaderExcerpt {
excerpt,
next_buffer_row,
next_excerpt_controls_present: *show_excerpt_controls,
});
}
Some(Block::FoldedBuffer {
@ -1476,7 +1440,6 @@ impl BlockSnapshot {
}) if top_row <= start => {
return Some(StickyHeaderExcerpt {
next_buffer_row: Some(end),
next_excerpt_controls_present: false,
excerpt,
});
}
@ -1785,7 +1748,6 @@ impl BlockChunks<'_> {
pub struct StickyHeaderExcerpt<'a> {
pub excerpt: &'a ExcerptInfo,
pub next_excerpt_controls_present: bool,
pub next_buffer_row: Option<u32>,
}
@ -2066,7 +2028,7 @@ mod tests {
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
let (wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
let block_ids = writer.insert(vec![
@ -2279,14 +2241,11 @@ mod tests {
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx);
let block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
let block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let snapshot = block_map.read(wraps_snapshot, Default::default());
// Each excerpt has a header above and footer below. Excerpts are also *separated* by a newline.
assert_eq!(
snapshot.text(),
"\n\nBuff\ner 1\n\n\n\nBuff\ner 2\n\n\n\nBuff\ner 3\n"
);
assert_eq!(snapshot.text(), "\nBuff\ner 1\n\nBuff\ner 2\n\nBuff\ner 3");
let blocks: Vec<_> = snapshot
.blocks_in_range(0..u32::MAX)
@ -2295,10 +2254,9 @@ mod tests {
assert_eq!(
blocks,
vec![
(0..2, BlockId::ExcerptBoundary(Some(excerpt_ids[0]))), // path, header
(4..7, BlockId::ExcerptBoundary(Some(excerpt_ids[1]))), // footer, path, header
(9..12, BlockId::ExcerptBoundary(Some(excerpt_ids[2]))), // footer, path, header
(14..15, BlockId::ExcerptBoundary(None)), // footer
(0..1, BlockId::ExcerptBoundary(Some(excerpt_ids[0]))), // path, header
(3..4, BlockId::ExcerptBoundary(Some(excerpt_ids[1]))), // path, header
(6..7, BlockId::ExcerptBoundary(Some(excerpt_ids[2]))), // path, header
]
);
}
@ -2317,7 +2275,7 @@ mod tests {
let (_tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
let (_wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
let block_ids = writer.insert(vec![
@ -2420,7 +2378,7 @@ mod tests {
let (_, wraps_snapshot) = cx.update(|cx| {
WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(60.)), cx)
});
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 0);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
writer.insert(vec![
@ -2464,7 +2422,7 @@ mod tests {
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, tab_size);
let (wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
let replace_block_id = writer.insert(vec![BlockProperties {
@ -2631,12 +2589,12 @@ mod tests {
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wrap_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wrap_snapshot.clone(), true, 2, 1, 1);
let mut block_map = BlockMap::new(wrap_snapshot.clone(), 2, 1);
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
assert_eq!(
blocks_snapshot.text(),
"\n\n\n111\n\n\n\n\n222\n\n\n333\n\n\n444\n\n\n\n\n555\n\n\n666\n"
"\n\n111\n\n\n222\n\n333\n\n444\n\n\n555\n\n666"
);
assert_eq!(
blocks_snapshot
@ -2644,30 +2602,21 @@ mod tests {
.map(|i| i.buffer_row)
.collect::<Vec<_>>(),
vec![
None,
None,
None,
Some(0),
None,
None,
None,
None,
Some(1),
None,
None,
Some(2),
None,
None,
Some(3),
None,
None,
None,
None,
Some(4),
None,
None,
Some(5),
None,
]
);
@ -2715,7 +2664,7 @@ mod tests {
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
assert_eq!(
blocks_snapshot.text(),
"\n\n\n111\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n"
"\n\n111\n\n\n\n222\n\n\n333\n\n444\n\n\n\n\n555\n\n666\n"
);
assert_eq!(
blocks_snapshot
@ -2723,35 +2672,26 @@ mod tests {
.map(|i| i.buffer_row)
.collect::<Vec<_>>(),
vec![
None,
None,
None,
Some(0),
None,
None,
None,
None,
None,
Some(1),
None,
None,
None,
Some(2),
None,
None,
Some(3),
None,
None,
None,
None,
None,
None,
Some(4),
None,
None,
Some(5),
None,
None,
]
);
@ -2793,7 +2733,7 @@ mod tests {
);
assert_eq!(
blocks_snapshot.text(),
"\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n"
"\n\n\n\n\n222\n\n\n333\n\n444\n\n\n\n\n555\n\n666\n"
);
assert_eq!(
blocks_snapshot
@ -2806,27 +2746,20 @@ mod tests {
None,
None,
None,
None,
Some(1),
None,
None,
None,
Some(2),
None,
None,
Some(3),
None,
None,
None,
None,
None,
None,
Some(4),
None,
None,
Some(5),
None,
None,
]
);
@ -2862,7 +2795,7 @@ mod tests {
.count(),
"Should have two folded blocks, producing headers"
);
assert_eq!(blocks_snapshot.text(), "\n\n\n\n\n\n\n\n555\n\n\n666\n\n");
assert_eq!(blocks_snapshot.text(), "\n\n\n\n\n\n\n555\n\n666\n");
assert_eq!(
blocks_snapshot
.row_infos(BlockRow(0))
@ -2876,13 +2809,10 @@ mod tests {
None,
None,
None,
None,
Some(4),
None,
None,
Some(5),
None,
None,
]
);
@ -2917,7 +2847,7 @@ mod tests {
);
assert_eq!(
blocks_snapshot.text(),
"\n\n\n\n111\n\n\n\n\n\n\n\n555\n\n\n666\n\n",
"\n\n\n111\n\n\n\n\n\n555\n\n666\n",
"Should have extra newline for 111 buffer, due to a new block added when it was folded"
);
assert_eq!(
@ -2929,21 +2859,16 @@ mod tests {
None,
None,
None,
None,
Some(0),
None,
None,
None,
None,
None,
None,
None,
Some(4),
None,
None,
Some(5),
None,
None,
]
);
@ -2974,7 +2899,7 @@ mod tests {
assert_eq!(
blocks_snapshot.text(),
"\n\n\n\n111\n\n\n\n\n",
"\n\n\n111\n\n\n\n",
"Should have a single, first buffer left after folding"
);
assert_eq!(
@ -2982,18 +2907,7 @@ mod tests {
.row_infos(BlockRow(0))
.map(|i| i.buffer_row)
.collect::<Vec<_>>(),
vec![
None,
None,
None,
None,
Some(0),
None,
None,
None,
None,
None,
]
vec![None, None, None, Some(0), None, None, None, None,]
);
}
@ -3020,10 +2934,10 @@ mod tests {
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wrap_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wrap_snapshot.clone(), true, 2, 1, 1);
let mut block_map = BlockMap::new(wrap_snapshot.clone(), 2, 1);
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
assert_eq!(blocks_snapshot.text(), "\n\n\n111\n");
assert_eq!(blocks_snapshot.text(), "\n\n111");
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
buffer.read_with(cx, |buffer, cx| {
@ -3077,11 +2991,9 @@ mod tests {
let font_size = px(14.0);
let buffer_start_header_height = rng.gen_range(1..=5);
let excerpt_header_height = rng.gen_range(1..=5);
let excerpt_footer_height = rng.gen_range(1..=5);
log::info!("Wrap width: {:?}", wrap_width);
log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
log::info!("Excerpt Footer Height: {:?}", excerpt_footer_height);
let is_singleton = rng.gen();
let buffer = if is_singleton {
let len = rng.gen_range(0..10);
@ -3108,10 +3020,8 @@ mod tests {
cx.update(|cx| WrapMap::new(tab_snapshot, font, font_size, wrap_width, cx));
let mut block_map = BlockMap::new(
wraps_snapshot,
true,
buffer_start_header_height,
excerpt_header_height,
excerpt_footer_height,
);
for _ in 0..operations {
@ -3329,8 +3239,6 @@ mod tests {
// Note that this needs to be synced with the related section in BlockMap::sync
expected_blocks.extend(BlockMap::header_and_footer_blocks(
true,
excerpt_footer_height,
buffer_start_header_height,
excerpt_header_height,
&buffer_snapshot,