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

@ -18,6 +18,7 @@ use language::{
};
use smallvec::SmallVec;
use std::{
any::type_name,
borrow::Cow,
cell::{Ref, RefCell},
cmp, fmt,
@ -173,17 +174,40 @@ pub struct MultiBufferSnapshot {
show_headers: bool,
}
/// A boundary between [`Excerpt`]s in a [`MultiBuffer`]
pub struct ExcerptBoundary {
pub struct ExcerptInfo {
pub id: ExcerptId,
pub row: MultiBufferRow,
pub buffer: BufferSnapshot,
pub buffer_id: BufferId,
pub range: ExcerptRange<text::Anchor>,
/// It's possible to have multiple excerpts in the same buffer,
/// and they are rendered together without a new File header.
///
/// This flag indicates that the excerpt is the first one in the buffer.
pub starts_new_buffer: bool,
}
impl std::fmt::Debug for ExcerptInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(type_name::<Self>())
.field("id", &self.id)
.field("buffer_id", &self.buffer_id)
.field("range", &self.range)
.finish()
}
}
/// A boundary between [`Excerpt`]s in a [`MultiBuffer`]
#[derive(Debug)]
pub struct ExcerptBoundary {
pub prev: Option<ExcerptInfo>,
pub next: Option<ExcerptInfo>,
/// The row in the `MultiBuffer` where the boundary is located
pub row: MultiBufferRow,
}
impl ExcerptBoundary {
pub fn starts_new_buffer(&self) -> bool {
match (self.prev.as_ref(), self.next.as_ref()) {
(None, _) => true,
(Some(_), None) => false,
(Some(prev), Some(next)) => prev.buffer_id != next.buffer_id,
}
}
}
/// A slice into a [`Buffer`] that is being edited in a [`MultiBuffer`].
@ -281,6 +305,30 @@ struct ExcerptBytes<'a> {
reversed: bool,
}
pub enum ExpandExcerptDirection {
Up,
Down,
UpAndDown,
}
impl ExpandExcerptDirection {
pub fn should_expand_up(&self) -> bool {
match self {
ExpandExcerptDirection::Up => true,
ExpandExcerptDirection::Down => false,
ExpandExcerptDirection::UpAndDown => true,
}
}
pub fn should_expand_down(&self) -> bool {
match self {
ExpandExcerptDirection::Up => false,
ExpandExcerptDirection::Down => true,
ExpandExcerptDirection::UpAndDown => true,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MultiBufferIndentGuide {
pub multibuffer_row_range: Range<MultiBufferRow>,
@ -1610,6 +1658,7 @@ impl MultiBuffer {
&mut self,
ids: impl IntoIterator<Item = ExcerptId>,
line_count: u32,
direction: ExpandExcerptDirection,
cx: &mut ModelContext<Self>,
) {
if line_count == 0 {
@ -1630,26 +1679,40 @@ impl MultiBuffer {
let mut excerpt = cursor.item().unwrap().clone();
let old_text_len = excerpt.text_summary.len;
let up_line_count = if direction.should_expand_up() {
line_count
} else {
0
};
let start_row = excerpt
.range
.context
.start
.to_point(&excerpt.buffer)
.row
.saturating_sub(line_count);
.saturating_sub(up_line_count);
let start_point = Point::new(start_row, 0);
excerpt.range.context.start = excerpt.buffer.anchor_before(start_point);
let end_point = excerpt.buffer.clip_point(
excerpt.range.context.end.to_point(&excerpt.buffer) + Point::new(line_count, 0),
let down_line_count = if direction.should_expand_down() {
line_count
} else {
0
};
let mut end_point = excerpt.buffer.clip_point(
excerpt.range.context.end.to_point(&excerpt.buffer)
+ Point::new(down_line_count, 0),
Bias::Left,
);
end_point.column = excerpt.buffer.line_len(end_point.row);
excerpt.range.context.end = excerpt.buffer.anchor_after(end_point);
excerpt.max_buffer_row = end_point.row;
excerpt.text_summary = excerpt
.buffer
.text_summary_for_range(start_point..end_point);
.text_summary_for_range(excerpt.range.context.clone());
let new_start_offset = new_excerpts.summary().text.len;
let old_start_offset = cursor.start().1;
@ -1920,7 +1983,12 @@ impl MultiBuffer {
log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
self.expand_excerpts(excerpts.iter().cloned(), line_count, cx);
self.expand_excerpts(
excerpts.iter().cloned(),
line_count,
ExpandExcerptDirection::UpAndDown,
cx,
);
continue;
}
@ -3018,24 +3086,37 @@ impl MultiBufferSnapshot {
cursor.next(&());
}
let mut prev_buffer_id = cursor.prev_item().map(|excerpt| excerpt.buffer_id);
let mut visited_end = false;
std::iter::from_fn(move || {
if self.singleton {
None
} else if bounds.contains(&cursor.start().0) {
let excerpt = cursor.item()?;
let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id;
let boundary = ExcerptBoundary {
let next = cursor.item().map(|excerpt| ExcerptInfo {
id: excerpt.id,
row: MultiBufferRow(cursor.start().1.row),
buffer: excerpt.buffer.clone(),
buffer_id: excerpt.buffer_id,
range: excerpt.range.clone(),
starts_new_buffer,
};
});
if next.is_none() {
if visited_end {
return None;
} else {
visited_end = true;
}
}
let prev = cursor.prev_item().map(|prev_excerpt| ExcerptInfo {
id: prev_excerpt.id,
buffer: prev_excerpt.buffer.clone(),
buffer_id: prev_excerpt.buffer_id,
range: prev_excerpt.range.clone(),
});
let row = MultiBufferRow(cursor.start().1.row);
prev_buffer_id = Some(excerpt.buffer_id);
cursor.next(&());
Some(boundary)
Some(ExcerptBoundary { row, prev, next })
} else {
None
}
@ -4537,15 +4618,16 @@ where
.peekable();
while let Some(range) = range_iter.next() {
let excerpt_start = Point::new(range.start.row.saturating_sub(context_line_count), 0);
// These + 1s ensure that we select the whole next line
let mut excerpt_end = Point::new(range.end.row + 1 + context_line_count, 0).min(max_point);
let row = (range.end.row + context_line_count).min(max_point.row);
let mut excerpt_end = Point::new(row, buffer.line_len(row));
let mut ranges_in_excerpt = 1;
while let Some(next_range) = range_iter.peek() {
if next_range.start.row <= excerpt_end.row + context_line_count {
excerpt_end =
Point::new(next_range.end.row + 1 + context_line_count, 0).min(max_point);
let row = (next_range.end.row + context_line_count).min(max_point.row);
excerpt_end = Point::new(row, buffer.line_len(row));
ranges_in_excerpt += 1;
range_iter.next();
} else {
@ -4866,15 +4948,17 @@ mod tests {
) -> Vec<(MultiBufferRow, String, bool)> {
snapshot
.excerpt_boundaries_in_range(range)
.map(|boundary| {
(
boundary.row,
boundary
.buffer
.text_for_range(boundary.range.context)
.collect::<String>(),
boundary.starts_new_buffer,
)
.filter_map(|boundary| {
let starts_new_buffer = boundary.starts_new_buffer();
boundary.next.map(|next| {
(
boundary.row,
next.buffer
.text_for_range(next.range.context)
.collect::<String>(),
starts_new_buffer,
)
})
})
.collect::<Vec<_>>()
}
@ -5006,8 +5090,33 @@ mod tests {
)
});
let snapshot = multibuffer.read(cx).snapshot(cx);
assert_eq!(
snapshot.text(),
concat!(
"ccc\n", //
"ddd\n", //
"eee", //
"\n", // End of excerpt
"ggg\n", //
"hhh\n", //
"iii", //
"\n", // End of excerpt
"ooo\n", //
"ppp\n", //
"qqq", // End of excerpt
)
);
drop(snapshot);
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.expand_excerpts(multibuffer.excerpt_ids(), 1, cx)
multibuffer.expand_excerpts(
multibuffer.excerpt_ids(),
1,
ExpandExcerptDirection::UpAndDown,
cx,
)
});
let snapshot = multibuffer.read(cx).snapshot(cx);
@ -5018,23 +5127,21 @@ mod tests {
assert_eq!(
snapshot.text(),
concat!(
"bbb\n", // Preserve newlines
"bbb\n", //
"ccc\n", //
"ddd\n", //
"eee\n", //
"fff\n", // <- Same as below
"\n", // Excerpt boundary
"fff\n", // <- Same as above
"fff\n", // End of excerpt
"fff\n", //
"ggg\n", //
"hhh\n", //
"iii\n", //
"jjj\n", //
"\n", //
"jjj\n", // End of excerpt
"nnn\n", //
"ooo\n", //
"ppp\n", //
"qqq\n", //
"rrr\n", //
"rrr", // End of excerpt
)
);
}
@ -5071,12 +5178,11 @@ mod tests {
"hhh\n", //
"iii\n", //
"jjj\n", //
"\n", //
"nnn\n", //
"ooo\n", //
"ppp\n", //
"qqq\n", //
"rrr\n", //
"rrr", //
)
);
@ -5088,7 +5194,7 @@ mod tests {
vec![
Point::new(2, 2)..Point::new(3, 2),
Point::new(6, 1)..Point::new(6, 3),
Point::new(12, 0)..Point::new(12, 0)
Point::new(11, 0)..Point::new(11, 0)
]
);
}
@ -5123,12 +5229,11 @@ mod tests {
"hhh\n", //
"iii\n", //
"jjj\n", //
"\n", //
"nnn\n", //
"ooo\n", //
"ppp\n", //
"qqq\n", //
"rrr\n", //
"rrr", //
)
);
@ -5140,7 +5245,7 @@ mod tests {
vec![
Point::new(2, 2)..Point::new(3, 2),
Point::new(6, 1)..Point::new(6, 3),
Point::new(12, 0)..Point::new(12, 0)
Point::new(11, 0)..Point::new(11, 0)
]
);
}
@ -5404,7 +5509,12 @@ mod tests {
.map(|id| excerpt_ids.iter().position(|i| i == id).unwrap())
.collect::<Vec<_>>();
log::info!("Expanding excerpts {excerpt_ixs:?} by {line_count} lines");
multibuffer.expand_excerpts(excerpts.iter().cloned(), line_count, cx);
multibuffer.expand_excerpts(
excerpts.iter().cloned(),
line_count,
ExpandExcerptDirection::UpAndDown,
cx,
);
if line_count > 0 {
for id in excerpts {
@ -5418,6 +5528,7 @@ mod tests {
Point::new(point_range.end.row + line_count, 0),
Bias::Left,
);
point_range.end.column = snapshot.line_len(point_range.end.row);
*range = snapshot.anchor_before(point_range.start)
..snapshot.anchor_after(point_range.end);
}