Improve context expansion (#10957)

Release Notes:

- Improved expand excerpt indicators to allow unidirectional expansion.
Also added the `editor::ExpandExcerptsUp` and
`editor::ExpandExcerptsDown` actions, which can both take a `lines`
parameter. Also added a `expand_excerpt_lines` setting which controls
the default number of lines that the indicators and actions use.

---------

Co-authored-by: conrad <conrad@zed.dev>
This commit is contained in:
Mikayla Maki 2024-05-26 16:30:09 -07:00 committed by GitHub
parent a0f91299dd
commit a9e3d4ec4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 904 additions and 328 deletions

View file

@ -12,7 +12,7 @@ use std::{
cell::RefCell,
cmp::{self, Ordering},
fmt::Debug,
ops::{Deref, DerefMut, Range},
ops::{Deref, DerefMut, Range, RangeBounds},
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
@ -31,8 +31,10 @@ pub struct BlockMap {
wrap_snapshot: RefCell<WrapSnapshot>,
blocks: Vec<Arc<Block>>,
transforms: RefCell<SumTree<Transform>>,
show_excerpt_controls: bool,
buffer_header_height: u8,
excerpt_header_height: u8,
excerpt_footer_height: u8,
}
pub struct BlockMapWriter<'a>(&'a mut BlockMap);
@ -92,6 +94,7 @@ pub struct BlockContext<'a, 'b> {
pub editor_style: &'b EditorStyle,
}
/// Whether the block should be considered above or below the anchor line
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum BlockDisposition {
Above,
@ -104,6 +107,17 @@ struct Transform {
block: Option<TransformBlock>,
}
pub(crate) enum BlockType {
Custom(BlockId),
Header,
Footer,
}
pub(crate) trait BlockLike {
fn block_type(&self) -> BlockType;
fn disposition(&self) -> BlockDisposition;
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
pub enum TransformBlock {
@ -114,7 +128,27 @@ pub enum TransformBlock {
range: ExcerptRange<text::Anchor>,
height: u8,
starts_new_buffer: bool,
show_excerpt_controls: bool,
},
ExcerptFooter {
id: ExcerptId,
disposition: BlockDisposition,
height: u8,
},
}
impl BlockLike for TransformBlock {
fn block_type(&self) -> BlockType {
match self {
TransformBlock::Custom(block) => BlockType::Custom(block.id),
TransformBlock::ExcerptHeader { .. } => BlockType::Header,
TransformBlock::ExcerptFooter { .. } => BlockType::Footer,
}
}
fn disposition(&self) -> BlockDisposition {
self.disposition()
}
}
impl TransformBlock {
@ -122,6 +156,7 @@ impl TransformBlock {
match self {
TransformBlock::Custom(block) => block.disposition,
TransformBlock::ExcerptHeader { .. } => BlockDisposition::Above,
TransformBlock::ExcerptFooter { disposition, .. } => *disposition,
}
}
@ -129,6 +164,7 @@ impl TransformBlock {
match self {
TransformBlock::Custom(block) => block.height,
TransformBlock::ExcerptHeader { height, .. } => *height,
TransformBlock::ExcerptFooter { height, .. } => *height,
}
}
}
@ -137,9 +173,23 @@ impl Debug for TransformBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Custom(block) => f.debug_struct("Custom").field("block", block).finish(),
Self::ExcerptHeader { buffer, .. } => f
Self::ExcerptHeader {
buffer,
starts_new_buffer,
id,
..
} => f
.debug_struct("ExcerptHeader")
.field("id", &id)
.field("path", &buffer.file().map(|f| f.path()))
.field("starts_new_buffer", &starts_new_buffer)
.finish(),
TransformBlock::ExcerptFooter {
id, disposition, ..
} => f
.debug_struct("ExcerptFooter")
.field("id", &id)
.field("disposition", &disposition)
.finish(),
}
}
@ -170,8 +220,10 @@ pub struct BlockBufferRows<'a> {
impl BlockMap {
pub fn new(
wrap_snapshot: WrapSnapshot,
show_excerpt_controls: bool,
buffer_header_height: u8,
excerpt_header_height: u8,
excerpt_footer_height: u8,
) -> Self {
let row_count = wrap_snapshot.max_point().row() + 1;
let map = Self {
@ -179,8 +231,10 @@ impl BlockMap {
blocks: Vec::new(),
transforms: RefCell::new(SumTree::from_item(Transform::isomorphic(row_count), &())),
wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
show_excerpt_controls,
buffer_header_height,
excerpt_header_height,
excerpt_footer_height,
};
map.sync(
&wrap_snapshot,
@ -364,49 +418,20 @@ impl BlockMap {
(position.row(), TransformBlock::Custom(block.clone()))
}),
);
if buffer.show_headers() {
blocks_in_edit.extend(
buffer
.excerpt_boundaries_in_range((start_bound, end_bound))
.map(|excerpt_boundary| {
(
wrap_snapshot
.make_wrap_point(
Point::new(excerpt_boundary.row.0, 0),
Bias::Left,
)
.row(),
TransformBlock::ExcerptHeader {
id: excerpt_boundary.id,
buffer: excerpt_boundary.buffer,
range: excerpt_boundary.range,
height: if excerpt_boundary.starts_new_buffer {
self.buffer_header_height
} else {
self.excerpt_header_height
},
starts_new_buffer: excerpt_boundary.starts_new_buffer,
},
)
}),
);
blocks_in_edit.extend(BlockMap::header_blocks(
self.show_excerpt_controls,
self.excerpt_footer_height,
self.buffer_header_height,
self.excerpt_header_height,
buffer,
(start_bound, end_bound),
wrap_snapshot,
));
}
// Place excerpt headers above custom blocks on the same row.
blocks_in_edit.sort_unstable_by(|(row_a, block_a), (row_b, block_b)| {
row_a.cmp(row_b).then_with(|| match (block_a, block_b) {
(
TransformBlock::ExcerptHeader { .. },
TransformBlock::ExcerptHeader { .. },
) => Ordering::Equal,
(TransformBlock::ExcerptHeader { .. }, _) => Ordering::Less,
(_, TransformBlock::ExcerptHeader { .. }) => Ordering::Greater,
(TransformBlock::Custom(block_a), TransformBlock::Custom(block_b)) => block_a
.disposition
.cmp(&block_b.disposition)
.then_with(|| block_a.id.cmp(&block_b.id)),
})
});
BlockMap::sort_blocks(&mut blocks_in_edit);
// For each of these blocks, insert a new isomorphic transform preceding the block,
// and then insert the block itself.
@ -449,6 +474,95 @@ impl BlockMap {
}
}
}
pub fn show_excerpt_controls(&self) -> bool {
self.show_excerpt_controls
}
pub fn header_blocks<'a, 'b: 'a, 'c: 'a + 'b, R, T>(
show_excerpt_controls: bool,
excerpt_footer_height: u8,
buffer_header_height: u8,
excerpt_header_height: u8,
buffer: &'b multi_buffer::MultiBufferSnapshot,
range: R,
wrap_snapshot: &'c WrapSnapshot,
) -> impl Iterator<Item = (u32, TransformBlock)> + 'b
where
R: RangeBounds<T>,
T: multi_buffer::ToOffset,
{
buffer
.excerpt_boundaries_in_range(range)
.flat_map(move |excerpt_boundary| {
let wrap_row = wrap_snapshot
.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
.row();
[
show_excerpt_controls
.then(|| {
excerpt_boundary.prev.as_ref().map(|prev| {
(
wrap_row,
TransformBlock::ExcerptFooter {
id: prev.id,
height: excerpt_footer_height,
disposition: if excerpt_boundary.next.is_some() {
BlockDisposition::Above
} else {
BlockDisposition::Below
},
},
)
})
})
.flatten(),
excerpt_boundary.next.map(|next| {
let starts_new_buffer = excerpt_boundary
.prev
.map_or(true, |prev| prev.buffer_id != next.buffer_id);
(
wrap_row,
TransformBlock::ExcerptHeader {
id: next.id,
buffer: next.buffer,
range: next.range,
height: if starts_new_buffer {
buffer_header_height
} else {
excerpt_header_height
},
starts_new_buffer,
show_excerpt_controls,
},
)
}),
]
})
.flatten()
}
pub(crate) fn sort_blocks<B: BlockLike>(blocks: &mut Vec<(u32, B)>) {
// Place excerpt headers and footers above custom blocks on the same row
blocks.sort_unstable_by(|(row_a, block_a), (row_b, block_b)| {
row_a.cmp(row_b).then_with(|| {
block_a
.disposition()
.cmp(&block_b.disposition())
.then_with(|| match ((block_a.block_type()), (block_b.block_type())) {
(BlockType::Footer, BlockType::Footer) => Ordering::Equal,
(BlockType::Footer, _) => Ordering::Less,
(_, BlockType::Footer) => Ordering::Greater,
(BlockType::Header, BlockType::Header) => Ordering::Equal,
(BlockType::Header, _) => Ordering::Less,
(_, BlockType::Header) => Ordering::Greater,
(BlockType::Custom(a_id), BlockType::Custom(b_id)) => a_id.cmp(&b_id),
})
})
});
}
}
fn push_isomorphic(tree: &mut SumTree<Transform>, rows: u32) {
@ -996,6 +1110,8 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
#[cfg(test)]
mod tests {
use std::env;
use super::*;
use crate::display_map::inlay_map::InlayMap;
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
@ -1003,7 +1119,6 @@ mod tests {
use multi_buffer::MultiBuffer;
use rand::prelude::*;
use settings::SettingsStore;
use std::env;
use util::RandomCharIter;
#[gpui::test]
@ -1034,7 +1149,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(), 1, 1);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
let block_ids = writer.insert(vec![
@ -1206,7 +1321,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(), 1, 1);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 0);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
writer.insert(vec![
@ -1252,9 +1367,11 @@ 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 buffer = if rng.gen() {
let len = rng.gen_range(0..10);
@ -1273,8 +1390,10 @@ mod tests {
.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), font_size, wrap_width, cx));
let mut block_map = BlockMap::new(
wraps_snapshot,
true,
buffer_start_header_height,
excerpt_header_height,
excerpt_footer_height,
);
let mut custom_blocks = Vec::new();
@ -1410,24 +1529,23 @@ mod tests {
},
)
}));
expected_blocks.extend(buffer_snapshot.excerpt_boundaries_in_range(0..).map(
|boundary| {
let position =
wraps_snapshot.make_wrap_point(Point::new(boundary.row.0, 0), Bias::Left);
(
position.row(),
ExpectedBlock::ExcerptHeader {
height: if boundary.starts_new_buffer {
buffer_start_header_height
} else {
excerpt_header_height
},
starts_new_buffer: boundary.starts_new_buffer,
},
)
},
));
expected_blocks.sort_unstable();
// Note that this needs to be synced with the related section in BlockMap::sync
expected_blocks.extend(
BlockMap::header_blocks(
true,
excerpt_footer_height,
buffer_start_header_height,
excerpt_header_height,
&buffer_snapshot,
0..,
&wraps_snapshot,
)
.map(|(row, block)| (row, block.into())),
);
BlockMap::sort_blocks(&mut expected_blocks);
let mut sorted_blocks_iter = expected_blocks.into_iter().peekable();
let input_buffer_rows = buffer_snapshot
@ -1593,12 +1711,16 @@ mod tests {
}
}
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
#[derive(Debug, Eq, PartialEq)]
enum ExpectedBlock {
ExcerptHeader {
height: u8,
starts_new_buffer: bool,
},
ExcerptFooter {
height: u8,
disposition: BlockDisposition,
},
Custom {
disposition: BlockDisposition,
id: BlockId,
@ -1606,11 +1728,26 @@ mod tests {
},
}
impl BlockLike for ExpectedBlock {
fn block_type(&self) -> BlockType {
match self {
ExpectedBlock::Custom { id, .. } => BlockType::Custom(*id),
ExpectedBlock::ExcerptHeader { .. } => BlockType::Header,
ExpectedBlock::ExcerptFooter { .. } => BlockType::Footer,
}
}
fn disposition(&self) -> BlockDisposition {
self.disposition()
}
}
impl ExpectedBlock {
fn height(&self) -> u8 {
match self {
ExpectedBlock::ExcerptHeader { height, .. } => *height,
ExpectedBlock::Custom { height, .. } => *height,
ExpectedBlock::ExcerptFooter { height, .. } => *height,
}
}
@ -1618,6 +1755,7 @@ mod tests {
match self {
ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above,
ExpectedBlock::Custom { disposition, .. } => *disposition,
ExpectedBlock::ExcerptFooter { disposition, .. } => *disposition,
}
}
}
@ -1638,6 +1776,14 @@ mod tests {
height,
starts_new_buffer,
},
TransformBlock::ExcerptFooter {
height,
disposition,
..
} => ExpectedBlock::ExcerptFooter {
height,
disposition,
},
}
}
}
@ -1654,6 +1800,7 @@ mod tests {
match self {
TransformBlock::Custom(block) => Some(block),
TransformBlock::ExcerptHeader { .. } => None,
TransformBlock::ExcerptFooter { .. } => None,
}
}
}