Detailed changes
@@ -1050,6 +1050,7 @@ fn editor_blocks(
.ok()?
}
+ Block::FoldedBuffer { .. } => FILE_HEADER.into(),
Block::ExcerptBoundary {
starts_new_buffer, ..
} => {
@@ -269,7 +269,7 @@ impl DisplayMap {
let start = buffer_snapshot.anchor_before(range.start);
let end = buffer_snapshot.anchor_after(range.end);
BlockProperties {
- placement: BlockPlacement::Replace(start..end),
+ placement: BlockPlacement::Replace(start..=end),
render,
height,
style,
@@ -336,6 +336,38 @@ impl DisplayMap {
block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
}
+ pub fn fold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext<Self>) {
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let edits = self.buffer_subscription.consume().into_inner();
+ let tab_size = Self::tab_size(&self.buffer, cx);
+ let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
+ let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+ let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+ let (snapshot, edits) = self
+ .wrap_map
+ .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+ let mut block_map = self.block_map.write(snapshot, edits);
+ block_map.fold_buffer(buffer_id, self.buffer.read(cx), cx)
+ }
+
+ pub fn unfold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext<Self>) {
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let edits = self.buffer_subscription.consume().into_inner();
+ let tab_size = Self::tab_size(&self.buffer, cx);
+ let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
+ let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+ let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+ let (snapshot, edits) = self
+ .wrap_map
+ .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+ let mut block_map = self.block_map.write(snapshot, edits);
+ block_map.unfold_buffer(buffer_id, self.buffer.read(cx), cx)
+ }
+
+ pub(crate) fn buffer_folded(&self, buffer_id: language::BufferId) -> bool {
+ self.block_map.folded_buffers.contains(&buffer_id)
+ }
+
pub fn insert_creases(
&mut self,
creases: impl IntoIterator<Item = Crease<Anchor>>,
@@ -712,7 +744,11 @@ impl DisplaySnapshot {
}
}
- pub fn next_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
+ pub fn next_line_boundary(
+ &self,
+ mut point: MultiBufferPoint,
+ ) -> (MultiBufferPoint, DisplayPoint) {
+ let original_point = point;
loop {
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
@@ -723,7 +759,7 @@ impl DisplaySnapshot {
let mut display_point = self.point_to_display_point(point, Bias::Right);
*display_point.column_mut() = self.line_len(display_point.row());
let next_point = self.display_point_to_point(display_point, Bias::Right);
- if next_point == point {
+ if next_point == point || original_point == point || original_point == next_point {
return (point, display_point);
}
point = next_point;
@@ -1081,10 +1117,6 @@ impl DisplaySnapshot {
|| self.fold_snapshot.is_line_folded(buffer_row)
}
- pub fn is_line_replaced(&self, buffer_row: MultiBufferRow) -> bool {
- self.block_snapshot.is_line_replaced(buffer_row)
- }
-
pub fn is_block_line(&self, display_row: DisplayRow) -> bool {
self.block_snapshot.is_block_line(BlockRow(display_row.0))
}
@@ -2231,7 +2263,7 @@ pub mod tests {
[BlockProperties {
placement: BlockPlacement::Replace(
buffer_snapshot.anchor_before(Point::new(1, 2))
- ..buffer_snapshot.anchor_after(Point::new(2, 3)),
+ ..=buffer_snapshot.anchor_after(Point::new(2, 3)),
),
height: 4,
style: BlockStyle::Fixed,
@@ -4,24 +4,25 @@ use super::{
};
use crate::{EditorStyle, GutterDimensions};
use collections::{Bound, HashMap, HashSet};
-use gpui::{AnyElement, EntityId, Pixels, WindowContext};
+use gpui::{AnyElement, AppContext, EntityId, Pixels, WindowContext};
use language::{Chunk, Patch, Point};
use multi_buffer::{
- Anchor, ExcerptId, ExcerptInfo, MultiBufferRow, MultiBufferSnapshot, ToOffset, ToPoint as _,
+ Anchor, ExcerptId, ExcerptInfo, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToOffset,
+ ToPoint as _,
};
use parking_lot::Mutex;
use std::{
cell::RefCell,
cmp::{self, Ordering},
fmt::Debug,
- ops::{Deref, DerefMut, Range, RangeBounds},
+ ops::{Deref, DerefMut, Range, RangeBounds, RangeInclusive},
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
},
};
use sum_tree::{Bias, SumTree, Summary, TreeMap};
-use text::Edit;
+use text::{BufferId, Edit};
use ui::ElementId;
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
@@ -40,6 +41,7 @@ pub struct BlockMap {
buffer_header_height: u32,
excerpt_header_height: u32,
excerpt_footer_height: u32,
+ pub(super) folded_buffers: HashSet<BufferId>,
}
pub struct BlockMapReader<'a> {
@@ -83,7 +85,7 @@ pub type RenderBlock = Arc<dyn Send + Sync + Fn(&mut BlockContext) -> AnyElement
pub enum BlockPlacement<T> {
Above(T),
Below(T),
- Replace(Range<T>),
+ Replace(RangeInclusive<T>),
}
impl<T> BlockPlacement<T> {
@@ -91,7 +93,7 @@ impl<T> BlockPlacement<T> {
match self {
BlockPlacement::Above(position) => position,
BlockPlacement::Below(position) => position,
- BlockPlacement::Replace(range) => &range.start,
+ BlockPlacement::Replace(range) => range.start(),
}
}
@@ -99,7 +101,7 @@ impl<T> BlockPlacement<T> {
match self {
BlockPlacement::Above(position) => position,
BlockPlacement::Below(position) => position,
- BlockPlacement::Replace(range) => &range.end,
+ BlockPlacement::Replace(range) => range.end(),
}
}
@@ -107,7 +109,7 @@ impl<T> BlockPlacement<T> {
match self {
BlockPlacement::Above(position) => BlockPlacement::Above(position),
BlockPlacement::Below(position) => BlockPlacement::Below(position),
- BlockPlacement::Replace(range) => BlockPlacement::Replace(&range.start..&range.end),
+ BlockPlacement::Replace(range) => BlockPlacement::Replace(range.start()..=range.end()),
}
}
@@ -115,7 +117,10 @@ impl<T> BlockPlacement<T> {
match self {
BlockPlacement::Above(position) => BlockPlacement::Above(f(position)),
BlockPlacement::Below(position) => BlockPlacement::Below(f(position)),
- BlockPlacement::Replace(range) => BlockPlacement::Replace(f(range.start)..f(range.end)),
+ BlockPlacement::Replace(range) => {
+ let (start, end) = range.into_inner();
+ BlockPlacement::Replace(f(start)..=f(end))
+ }
}
}
}
@@ -134,21 +139,21 @@ impl BlockPlacement<Anchor> {
anchor_a.cmp(anchor_b, buffer).then(Ordering::Greater)
}
(BlockPlacement::Above(anchor), BlockPlacement::Replace(range)) => {
- anchor.cmp(&range.start, buffer).then(Ordering::Less)
+ anchor.cmp(range.start(), buffer).then(Ordering::Less)
}
(BlockPlacement::Replace(range), BlockPlacement::Above(anchor)) => {
- range.start.cmp(anchor, buffer).then(Ordering::Greater)
+ range.start().cmp(anchor, buffer).then(Ordering::Greater)
}
(BlockPlacement::Below(anchor), BlockPlacement::Replace(range)) => {
- anchor.cmp(&range.start, buffer).then(Ordering::Greater)
+ anchor.cmp(range.start(), buffer).then(Ordering::Greater)
}
(BlockPlacement::Replace(range), BlockPlacement::Below(anchor)) => {
- range.start.cmp(anchor, buffer).then(Ordering::Less)
+ range.start().cmp(anchor, buffer).then(Ordering::Less)
}
(BlockPlacement::Replace(range_a), BlockPlacement::Replace(range_b)) => range_a
- .start
- .cmp(&range_b.start, buffer)
- .then_with(|| range_b.end.cmp(&range_a.end, buffer)),
+ .start()
+ .cmp(range_b.start(), buffer)
+ .then_with(|| range_b.end().cmp(range_a.end(), buffer)),
}
}
@@ -168,8 +173,8 @@ impl BlockPlacement<Anchor> {
Some(BlockPlacement::Below(wrap_row))
}
BlockPlacement::Replace(range) => {
- let mut start = range.start.to_point(buffer_snapshot);
- let mut end = range.end.to_point(buffer_snapshot);
+ let mut start = range.start().to_point(buffer_snapshot);
+ let mut end = range.end().to_point(buffer_snapshot);
if start == end {
None
} else {
@@ -179,50 +184,13 @@ impl BlockPlacement<Anchor> {
end.column = buffer_snapshot.line_len(MultiBufferRow(end.row));
let end_wrap_row =
WrapRow(wrap_snapshot.make_wrap_point(end, Bias::Left).row());
- Some(BlockPlacement::Replace(start_wrap_row..end_wrap_row))
+ Some(BlockPlacement::Replace(start_wrap_row..=end_wrap_row))
}
}
}
}
}
-impl Ord for BlockPlacement<WrapRow> {
- fn cmp(&self, other: &Self) -> Ordering {
- match (self, other) {
- (BlockPlacement::Above(row_a), BlockPlacement::Above(row_b))
- | (BlockPlacement::Below(row_a), BlockPlacement::Below(row_b)) => row_a.cmp(row_b),
- (BlockPlacement::Above(row_a), BlockPlacement::Below(row_b)) => {
- row_a.cmp(row_b).then(Ordering::Less)
- }
- (BlockPlacement::Below(row_a), BlockPlacement::Above(row_b)) => {
- row_a.cmp(row_b).then(Ordering::Greater)
- }
- (BlockPlacement::Above(row), BlockPlacement::Replace(range)) => {
- row.cmp(&range.start).then(Ordering::Less)
- }
- (BlockPlacement::Replace(range), BlockPlacement::Above(row)) => {
- range.start.cmp(row).then(Ordering::Greater)
- }
- (BlockPlacement::Below(row), BlockPlacement::Replace(range)) => {
- row.cmp(&range.start).then(Ordering::Greater)
- }
- (BlockPlacement::Replace(range), BlockPlacement::Below(row)) => {
- range.start.cmp(row).then(Ordering::Less)
- }
- (BlockPlacement::Replace(range_a), BlockPlacement::Replace(range_b)) => range_a
- .start
- .cmp(&range_b.start)
- .then_with(|| range_b.end.cmp(&range_a.end)),
- }
- }
-}
-
-impl PartialOrd for BlockPlacement<WrapRow> {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- Some(self.cmp(other))
- }
-}
-
pub struct CustomBlock {
id: CustomBlockId,
placement: BlockPlacement<Anchor>,
@@ -272,6 +240,7 @@ pub struct BlockContext<'a, 'b> {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub enum BlockId {
ExcerptBoundary(Option<ExcerptId>),
+ FoldedBuffer(ExcerptId),
Custom(CustomBlockId),
}
@@ -283,6 +252,7 @@ impl From<BlockId> for ElementId {
Some(id) => ("ExcerptBoundary", EntityId::from(id)).into(),
None => "LastExcerptBoundary".into(),
},
+ BlockId::FoldedBuffer(id) => ("FoldedBuffer", EntityId::from(id)).into(),
}
}
}
@@ -292,6 +262,7 @@ impl std::fmt::Display for BlockId {
match self {
Self::Custom(id) => write!(f, "Block({id:?})"),
Self::ExcerptBoundary(id) => write!(f, "ExcerptHeader({id:?})"),
+ Self::FoldedBuffer(id) => write!(f, "FoldedBuffer({id:?})"),
}
}
}
@@ -306,6 +277,12 @@ struct Transform {
#[derive(Clone)]
pub enum Block {
Custom(Arc<CustomBlock>),
+ FoldedBuffer {
+ first_excerpt: ExcerptInfo,
+ prev_excerpt: Option<ExcerptInfo>,
+ height: u32,
+ show_excerpt_controls: bool,
+ },
ExcerptBoundary {
prev_excerpt: Option<ExcerptInfo>,
next_excerpt: Option<ExcerptInfo>,
@@ -322,26 +299,28 @@ impl Block {
Block::ExcerptBoundary { next_excerpt, .. } => {
BlockId::ExcerptBoundary(next_excerpt.as_ref().map(|info| info.id))
}
+ Block::FoldedBuffer { first_excerpt, .. } => BlockId::FoldedBuffer(first_excerpt.id),
}
}
pub fn height(&self) -> u32 {
match self {
Block::Custom(block) => block.height,
- Block::ExcerptBoundary { height, .. } => *height,
+ Block::ExcerptBoundary { height, .. } | Block::FoldedBuffer { height, .. } => *height,
}
}
pub fn style(&self) -> BlockStyle {
match self {
Block::Custom(block) => block.style,
- Block::ExcerptBoundary { .. } => BlockStyle::Sticky,
+ Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => BlockStyle::Sticky,
}
}
fn place_above(&self) -> bool {
match self {
Block::Custom(block) => matches!(block.placement, BlockPlacement::Above(_)),
+ Block::FoldedBuffer { .. } => false,
Block::ExcerptBoundary { next_excerpt, .. } => next_excerpt.is_some(),
}
}
@@ -349,6 +328,7 @@ impl Block {
fn place_below(&self) -> bool {
match self {
Block::Custom(block) => matches!(block.placement, BlockPlacement::Below(_)),
+ Block::FoldedBuffer { .. } => false,
Block::ExcerptBoundary { next_excerpt, .. } => next_excerpt.is_none(),
}
}
@@ -356,15 +336,36 @@ impl Block {
fn is_replacement(&self) -> bool {
match self {
Block::Custom(block) => matches!(block.placement, BlockPlacement::Replace(_)),
+ Block::FoldedBuffer { .. } => true,
Block::ExcerptBoundary { .. } => false,
}
}
+
+ fn is_header(&self) -> bool {
+ match self {
+ Block::Custom(_) => false,
+ Block::FoldedBuffer { .. } => true,
+ Block::ExcerptBoundary { .. } => true,
+ }
+ }
}
impl Debug for Block {
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::FoldedBuffer {
+ 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,
next_excerpt,
@@ -372,9 +373,9 @@ impl Debug for Block {
..
} => f
.debug_struct("ExcerptBoundary")
- .field("prev_excerpt", &prev_excerpt)
- .field("next_excerpt", &next_excerpt)
- .field("starts_new_buffer", &starts_new_buffer)
+ .field("prev_excerpt", prev_excerpt)
+ .field("next_excerpt", next_excerpt)
+ .field("starts_new_buffer", starts_new_buffer)
.finish(),
}
}
@@ -420,6 +421,7 @@ impl BlockMap {
next_block_id: AtomicUsize::new(0),
custom_blocks: Vec::new(),
custom_blocks_by_id: TreeMap::default(),
+ folded_buffers: HashSet::default(),
transforms: RefCell::new(transforms),
wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
show_excerpt_controls,
@@ -495,13 +497,20 @@ impl BlockMap {
let mut old_start = WrapRow(edit.old.start);
let mut new_start = WrapRow(edit.new.start);
- // Preserve transforms that:
- // * strictly precedes this edit
- // * isomorphic or replace transforms that end *at* the start of the edit
- // * below blocks that end at the start of the edit
+ // Only preserve transforms that:
+ // * Strictly precedes this edit
+ // * Isomorphic transforms that end *at* the start of the edit
+ // * Below blocks that end at the start of the edit
+ // However, if we hit a replace block that ends at the start of the edit we want to reconstruct it.
new_transforms.append(cursor.slice(&old_start, Bias::Left, &()), &());
if let Some(transform) = cursor.item() {
- if transform.summary.input_rows > 0 && cursor.end(&()) == old_start {
+ if transform.summary.input_rows > 0
+ && cursor.end(&()) == old_start
+ && transform
+ .block
+ .as_ref()
+ .map_or(true, |b| !b.is_replacement())
+ {
// Preserve the transform (push and next)
new_transforms.push(transform.clone(), &());
cursor.next(&());
@@ -521,7 +530,6 @@ impl BlockMap {
// Ensure the edit starts at a transform boundary.
// If the edit starts within an isomorphic transform, preserve its prefix
// If the edit lands within a replacement block, expand the edit to include the start of the replaced input range
- let mut preserved_blocks_above_edit = false;
let transform = cursor.item().unwrap();
let transform_rows_before_edit = old_start.0 - cursor.start().0;
if transform_rows_before_edit > 0 {
@@ -539,9 +547,6 @@ impl BlockMap {
debug_assert!(transform.summary.input_rows > 0);
old_start.0 -= transform_rows_before_edit;
new_start.0 -= transform_rows_before_edit;
- // The blocks *above* it are already in the new transforms, so
- // we don't need to re-insert them when querying blocks.
- preserved_blocks_above_edit = true;
}
}
@@ -643,6 +648,7 @@ impl BlockMap {
self.buffer_header_height,
self.excerpt_header_height,
buffer,
+ &self.folded_buffers,
(start_bound, end_bound),
wrap_snapshot,
));
@@ -653,12 +659,6 @@ impl BlockMap {
// For each of these blocks, insert a new isomorphic transform preceding the block,
// and then insert the block itself.
for (block_placement, block) in blocks_in_edit.drain(..) {
- if preserved_blocks_above_edit
- && block_placement == BlockPlacement::Above(new_start)
- {
- continue;
- }
-
let mut summary = TransformSummary {
input_rows: 0,
output_rows: block.height(),
@@ -675,8 +675,8 @@ impl BlockMap {
rows_before_block = (position.0 + 1) - new_transforms.summary().input_rows;
}
BlockPlacement::Replace(range) => {
- rows_before_block = range.start.0 - new_transforms.summary().input_rows;
- summary.input_rows = range.end.0 - range.start.0 + 1;
+ rows_before_block = range.start().0 - new_transforms.summary().input_rows;
+ summary.input_rows = range.end().0 - range.start().0 + 1;
}
}
@@ -719,131 +719,208 @@ impl BlockMap {
self.show_excerpt_controls
}
- fn header_and_footer_blocks<'a, 'b: 'a, 'c: 'a + 'b, R, T>(
+ #[allow(clippy::too_many_arguments)]
+ 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: &'b multi_buffer::MultiBufferSnapshot,
+ buffer: &'a multi_buffer::MultiBufferSnapshot,
+ folded_buffers: &'a HashSet<BufferId>,
range: R,
- wrap_snapshot: &'c WrapSnapshot,
- ) -> impl Iterator<Item = (BlockPlacement<WrapRow>, Block)> + 'b
+ wrap_snapshot: &'a WrapSnapshot,
+ ) -> impl Iterator<Item = (BlockPlacement<WrapRow>, Block)> + 'a
where
R: RangeBounds<T>,
T: multi_buffer::ToOffset,
{
- buffer
- .excerpt_boundaries_in_range(range)
- .filter_map(move |excerpt_boundary| {
- let wrap_row;
- if excerpt_boundary.next.is_some() {
- wrap_row = wrap_snapshot
- .make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
- .row();
- } else {
- wrap_row = wrap_snapshot
- .make_wrap_point(
- Point::new(
- excerpt_boundary.row.0,
- buffer.line_len(excerpt_boundary.row),
- ),
- Bias::Left,
- )
- .row();
- }
+ let mut boundaries = buffer.excerpt_boundaries_in_range(range).peekable();
- let starts_new_buffer = match (&excerpt_boundary.prev, &excerpt_boundary.next) {
- (_, None) => false,
- (None, Some(_)) => true,
- (Some(prev), Some(next)) => prev.buffer_id != next.buffer_id,
- };
+ std::iter::from_fn(move || {
+ let excerpt_boundary = boundaries.next()?;
+ let wrap_row = if excerpt_boundary.next.is_some() {
+ wrap_snapshot.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
+ } else {
+ wrap_snapshot.make_wrap_point(
+ Point::new(
+ excerpt_boundary.row.0,
+ buffer.line_len(excerpt_boundary.row),
+ ),
+ Bias::Left,
+ )
+ }
+ .row();
- let mut height = 0;
- if excerpt_boundary.prev.is_some() {
- if show_excerpt_controls {
- height += excerpt_footer_height;
+ let new_buffer_id = match (&excerpt_boundary.prev, &excerpt_boundary.next) {
+ (_, None) => None,
+ (None, Some(next)) => Some(next.buffer_id),
+ (Some(prev), Some(next)) => {
+ if prev.buffer_id != next.buffer_id {
+ Some(next.buffer_id)
+ } else {
+ None
}
}
- if excerpt_boundary.next.is_some() {
- if starts_new_buffer {
- height += buffer_header_height;
- if show_excerpt_controls {
- height += excerpt_header_height;
+ };
+
+ let prev_excerpt = excerpt_boundary
+ .prev
+ .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();
+ if folded_buffers.contains(&new_buffer_id) {
+ let mut buffer_end = Point::new(excerpt_boundary.row.0, 0)
+ + excerpt_boundary.next.as_ref().unwrap().text_summary.lines;
+
+ while let Some(next_boundary) = boundaries.peek() {
+ if let Some(next_excerpt_boundary) = &next_boundary.next {
+ if next_excerpt_boundary.buffer_id == new_buffer_id {
+ buffer_end = Point::new(next_boundary.row.0, 0)
+ + next_excerpt_boundary.text_summary.lines;
+ } else {
+ break;
+ }
}
- } else {
- height += excerpt_header_height;
+
+ boundaries.next();
}
+
+ let wrap_end_row = wrap_snapshot.make_wrap_point(buffer_end, Bias::Right).row();
+
+ return Some((
+ BlockPlacement::Replace(WrapRow(wrap_row)..=WrapRow(wrap_end_row)),
+ Block::FoldedBuffer {
+ prev_excerpt,
+ height: height + buffer_header_height,
+ show_excerpt_controls,
+ first_excerpt,
+ },
+ ));
}
+ }
- if height == 0 {
- return None;
+ 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;
}
+ }
- Some((
- if excerpt_boundary.next.is_some() {
- BlockPlacement::Above(WrapRow(wrap_row))
- } else {
- BlockPlacement::Below(WrapRow(wrap_row))
- },
- Block::ExcerptBoundary {
- prev_excerpt: excerpt_boundary.prev,
- next_excerpt: excerpt_boundary.next,
- height,
- starts_new_buffer,
- show_excerpt_controls,
- },
- ))
- })
+ if height == 0 {
+ return None;
+ }
+
+ Some((
+ if excerpt_boundary.next.is_some() {
+ BlockPlacement::Above(WrapRow(wrap_row))
+ } else {
+ BlockPlacement::Below(WrapRow(wrap_row))
+ },
+ Block::ExcerptBoundary {
+ prev_excerpt,
+ next_excerpt: excerpt_boundary.next,
+ height,
+ starts_new_buffer: new_buffer_id.is_some(),
+ show_excerpt_controls,
+ },
+ ))
+ })
}
fn sort_blocks(blocks: &mut Vec<(BlockPlacement<WrapRow>, Block)>) {
blocks.sort_unstable_by(|(placement_a, block_a), (placement_b, block_b)| {
- placement_a
- .cmp(&placement_b)
- .then_with(|| match (block_a, block_b) {
- (
- Block::ExcerptBoundary {
- next_excerpt: next_excerpt_a,
- ..
- },
- Block::ExcerptBoundary {
- next_excerpt: next_excerpt_b,
- ..
- },
- ) => next_excerpt_a
- .as_ref()
- .map(|excerpt| excerpt.id)
- .cmp(&next_excerpt_b.as_ref().map(|excerpt| excerpt.id)),
- (Block::ExcerptBoundary { next_excerpt, .. }, Block::Custom(_)) => {
- if next_excerpt.is_some() {
+ let placement_comparison = match (placement_a, placement_b) {
+ (BlockPlacement::Above(row_a), BlockPlacement::Above(row_b))
+ | (BlockPlacement::Below(row_a), BlockPlacement::Below(row_b)) => row_a.cmp(row_b),
+ (BlockPlacement::Above(row_a), BlockPlacement::Below(row_b)) => {
+ row_a.cmp(row_b).then(Ordering::Less)
+ }
+ (BlockPlacement::Below(row_a), BlockPlacement::Above(row_b)) => {
+ row_a.cmp(row_b).then(Ordering::Greater)
+ }
+ (BlockPlacement::Above(row), BlockPlacement::Replace(range)) => {
+ row.cmp(range.start()).then(Ordering::Greater)
+ }
+ (BlockPlacement::Replace(range), BlockPlacement::Above(row)) => {
+ range.start().cmp(row).then(Ordering::Less)
+ }
+ (BlockPlacement::Below(row), BlockPlacement::Replace(range)) => {
+ row.cmp(range.start()).then(Ordering::Greater)
+ }
+ (BlockPlacement::Replace(range), BlockPlacement::Below(row)) => {
+ range.start().cmp(row).then(Ordering::Less)
+ }
+ (BlockPlacement::Replace(range_a), BlockPlacement::Replace(range_b)) => range_a
+ .start()
+ .cmp(range_b.start())
+ .then_with(|| range_b.end().cmp(range_a.end()))
+ .then_with(|| {
+ if block_a.is_header() {
Ordering::Less
- } else {
- Ordering::Greater
- }
- }
- (Block::Custom(_), Block::ExcerptBoundary { next_excerpt, .. }) => {
- if next_excerpt.is_some() {
+ } else if block_b.is_header() {
Ordering::Greater
} else {
- Ordering::Less
+ Ordering::Equal
}
+ }),
+ };
+ placement_comparison.then_with(|| match (block_a, block_b) {
+ (
+ Block::ExcerptBoundary {
+ next_excerpt: next_excerpt_a,
+ ..
+ },
+ Block::ExcerptBoundary {
+ next_excerpt: next_excerpt_b,
+ ..
+ },
+ ) => next_excerpt_a
+ .as_ref()
+ .map(|excerpt| excerpt.id)
+ .cmp(&next_excerpt_b.as_ref().map(|excerpt| excerpt.id)),
+ (Block::ExcerptBoundary { next_excerpt, .. }, Block::Custom(_)) => {
+ if next_excerpt.is_some() {
+ Ordering::Less
+ } else {
+ Ordering::Greater
}
- (Block::Custom(block_a), Block::Custom(block_b)) => block_a
- .priority
- .cmp(&block_b.priority)
- .then_with(|| block_a.id.cmp(&block_b.id)),
- })
+ }
+ (Block::Custom(_), Block::ExcerptBoundary { next_excerpt, .. }) => {
+ if next_excerpt.is_some() {
+ Ordering::Greater
+ } else {
+ Ordering::Less
+ }
+ }
+ (Block::Custom(block_a), Block::Custom(block_b)) => block_a
+ .priority
+ .cmp(&block_b.priority)
+ .then_with(|| block_a.id.cmp(&block_b.id)),
+ _ => {
+ unreachable!()
+ }
+ })
});
- blocks.dedup_by(|(right, _), (left, _)| match (left, right) {
- (BlockPlacement::Replace(range), BlockPlacement::Above(row)) => {
- range.start < *row && range.end >= *row
- }
- (BlockPlacement::Replace(range), BlockPlacement::Below(row)) => {
- range.start <= *row && range.end > *row
- }
+ blocks.dedup_by(|right, left| match (left.0.clone(), right.0.clone()) {
+ (BlockPlacement::Replace(range), BlockPlacement::Above(row))
+ | (BlockPlacement::Replace(range), BlockPlacement::Below(row)) => range.contains(&row),
(BlockPlacement::Replace(range_a), BlockPlacement::Replace(range_b)) => {
- if range_a.end >= range_b.start && range_a.start <= range_b.end {
- range_a.end = range_a.end.max(range_b.end);
+ if range_a.end() >= range_b.start() && range_a.start() <= range_b.end() {
+ left.0 = BlockPlacement::Replace(
+ *range_a.start()..=*range_a.end().max(range_b.end()),
+ );
true
} else {
false
@@ -1149,6 +1226,50 @@ impl<'a> BlockMapWriter<'a> {
self.remove(blocks_to_remove);
}
+ pub fn fold_buffer(
+ &mut self,
+ buffer_id: BufferId,
+ multi_buffer: &MultiBuffer,
+ cx: &AppContext,
+ ) {
+ self.0.folded_buffers.insert(buffer_id);
+ self.recompute_blocks_for_buffer(buffer_id, multi_buffer, cx);
+ }
+
+ pub fn unfold_buffer(
+ &mut self,
+ buffer_id: BufferId,
+ multi_buffer: &MultiBuffer,
+ cx: &AppContext,
+ ) {
+ self.0.folded_buffers.remove(&buffer_id);
+ self.recompute_blocks_for_buffer(buffer_id, multi_buffer, cx);
+ }
+
+ fn recompute_blocks_for_buffer(
+ &mut self,
+ buffer_id: BufferId,
+ multi_buffer: &MultiBuffer,
+ cx: &AppContext,
+ ) {
+ let wrap_snapshot = self.0.wrap_snapshot.borrow().clone();
+
+ let mut edits = Patch::default();
+ for range in multi_buffer.excerpt_ranges_for_buffer(buffer_id, cx) {
+ let last_edit_row = cmp::min(
+ wrap_snapshot.make_wrap_point(range.end, Bias::Right).row() + 1,
+ wrap_snapshot.max_point().row(),
+ ) + 1;
+ let range = wrap_snapshot.make_wrap_point(range.start, Bias::Left).row()..last_edit_row;
+ edits.push(Edit {
+ old: range.clone(),
+ new: range,
+ });
+ }
+
+ self.0.sync(&wrap_snapshot, edits);
+ }
+
fn blocks_intersecting_buffer_range(
&self,
range: Range<usize>,
@@ -1292,42 +1413,44 @@ impl BlockSnapshot {
pub fn block_for_id(&self, block_id: BlockId) -> Option<Block> {
let buffer = self.wrap_snapshot.buffer_snapshot();
-
- match block_id {
+ let wrap_point = match block_id {
BlockId::Custom(custom_block_id) => {
let custom_block = self.custom_blocks_by_id.get(&custom_block_id)?;
- Some(Block::Custom(custom_block.clone()))
+ return Some(Block::Custom(custom_block.clone()));
}
BlockId::ExcerptBoundary(next_excerpt_id) => {
- let wrap_point;
if let Some(next_excerpt_id) = next_excerpt_id {
let excerpt_range = buffer.range_for_excerpt::<Point>(next_excerpt_id)?;
- wrap_point = self
- .wrap_snapshot
- .make_wrap_point(excerpt_range.start, Bias::Left);
+ self.wrap_snapshot
+ .make_wrap_point(excerpt_range.start, Bias::Left)
} else {
- wrap_point = self
- .wrap_snapshot
- .make_wrap_point(buffer.max_point(), Bias::Left);
+ self.wrap_snapshot
+ .make_wrap_point(buffer.max_point(), Bias::Left)
}
+ }
+ BlockId::FoldedBuffer(excerpt_id) => self.wrap_snapshot.make_wrap_point(
+ buffer.range_for_excerpt::<Point>(excerpt_id)?.start,
+ Bias::Left,
+ ),
+ };
+ let wrap_row = WrapRow(wrap_point.row());
- let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&());
- cursor.seek(&WrapRow(wrap_point.row()), Bias::Left, &());
- while let Some(transform) = cursor.item() {
- if let Some(block) = transform.block.as_ref() {
- if block.id() == block_id {
- return Some(block.clone());
- }
- } else if cursor.start().0 > WrapRow(wrap_point.row()) {
- break;
- }
+ let mut cursor = self.transforms.cursor::<WrapRow>(&());
+ cursor.seek(&wrap_row, Bias::Left, &());
- cursor.next(&());
+ while let Some(transform) = cursor.item() {
+ if let Some(block) = transform.block.as_ref() {
+ if block.id() == block_id {
+ return Some(block.clone());
}
-
- None
+ } else if *cursor.start() > wrap_row {
+ break;
}
+
+ cursor.next(&());
}
+
+ None
}
pub fn max_point(&self) -> BlockPoint {
@@ -1421,11 +1544,10 @@ impl BlockSnapshot {
let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&());
cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &());
cursor.item().map_or(false, |transform| {
- if let Some(Block::Custom(block)) = transform.block.as_ref() {
- matches!(block.placement, BlockPlacement::Replace(_))
- } else {
- false
- }
+ transform
+ .block
+ .as_ref()
+ .map_or(false, |block| block.is_replacement())
})
}
@@ -1447,13 +1569,13 @@ impl BlockSnapshot {
let input_end = Point::new(input_end_row.0, 0);
match transform.block.as_ref() {
- Some(Block::Custom(block))
- if matches!(block.placement, BlockPlacement::Replace(_)) =>
- {
- if ((bias == Bias::Left || search_left) && output_start <= point.0)
- || (!search_left && output_start >= point.0)
- {
- return BlockPoint(output_start);
+ Some(block) => {
+ if block.is_replacement() {
+ if ((bias == Bias::Left || search_left) && output_start <= point.0)
+ || (!search_left && output_start >= point.0)
+ {
+ return BlockPoint(output_start);
+ }
}
}
None => {
@@ -1472,7 +1594,6 @@ impl BlockSnapshot {
return BlockPoint(output_start + input_overshoot);
}
}
- _ => {}
}
if search_left {
@@ -1682,7 +1803,11 @@ impl<'a> Iterator for BlockBufferRows<'a> {
let transform = self.transforms.item()?;
if let Some(block) = transform.block.as_ref() {
if block.is_replacement() && self.transforms.start().0 == self.output_row {
- Some(self.input_buffer_rows.next().unwrap())
+ if matches!(block, Block::FoldedBuffer { .. }) {
+ Some(None)
+ } else {
+ Some(self.input_buffer_rows.next().unwrap())
+ }
} else {
Some(None)
}
@@ -1806,6 +1931,7 @@ mod tests {
fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap, wrap_map::WrapMap,
};
use gpui::{div, font, px, AppContext, Context as _, Element};
+ use itertools::Itertools;
use language::{Buffer, Capability};
use multi_buffer::{ExcerptRange, MultiBuffer};
use rand::prelude::*;
@@ -2239,16 +2365,16 @@ mod tests {
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
- writer.insert(vec![BlockProperties {
+ let replace_block_id = writer.insert(vec![BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Replace(
buffer_snapshot.anchor_after(Point::new(1, 3))
- ..buffer_snapshot.anchor_before(Point::new(3, 1)),
+ ..=buffer_snapshot.anchor_before(Point::new(3, 1)),
),
height: 4,
render: Arc::new(|_| div().into_any()),
priority: 0,
- }]);
+ }])[0];
let blocks_snapshot = block_map.read(wraps_snapshot, Default::default());
assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5");
@@ -2273,7 +2399,7 @@ mod tests {
buffer.edit(
[(
Point::new(1, 5)..Point::new(1, 5),
- "\nline 6\nline7\nline 8\nline 9",
+ "\nline 2.1\nline2.2\nline 2.3\nline 2.4",
)],
None,
cx,
@@ -2292,9 +2418,16 @@ mod tests {
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5");
- // Ensure blocks inserted above the start or below the end of the replaced region are shown.
+ // Blocks inserted right above the start or right below the end of the replaced region are hidden.
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
writer.insert(vec![
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(0, 3))),
+ height: 1,
+ render: Arc::new(|_| div().into_any()),
+ priority: 0,
+ },
BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 3))),
@@ -2311,7 +2444,7 @@ mod tests {
},
]);
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
- assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\n\n\nline5");
+ assert_eq!(blocks_snapshot.text(), "\nline1\n\n\n\n\nline5");
// Ensure blocks inserted *inside* replaced region are hidden.
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -2338,8 +2471,470 @@ mod tests {
priority: 0,
},
]);
- let blocks_snapshot = block_map.read(wraps_snapshot, Default::default());
- assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\n\n\nline5");
+ let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
+ assert_eq!(blocks_snapshot.text(), "\nline1\n\n\n\n\nline5");
+
+ // Removing the replace block shows all the hidden blocks again.
+ let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
+ writer.remove(HashSet::from_iter([replace_block_id]));
+ let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
+ assert_eq!(
+ blocks_snapshot.text(),
+ "\nline1\n\nline2\n\n\nline 2.1\nline2.2\nline 2.3\nline 2.4\n\nline4\n\nline5"
+ );
+ }
+
+ #[gpui::test]
+ fn test_custom_blocks_inside_buffer_folds(cx: &mut gpui::TestAppContext) {
+ cx.update(init_test);
+
+ let text = "111\n222\n333\n444\n555\n666";
+
+ let buffer = cx.update(|cx| {
+ MultiBuffer::build_multi(
+ [
+ (text, vec![Point::new(0, 0)..Point::new(0, 3)]),
+ (
+ text,
+ vec![
+ Point::new(1, 0)..Point::new(1, 3),
+ Point::new(2, 0)..Point::new(2, 3),
+ Point::new(3, 0)..Point::new(3, 3),
+ ],
+ ),
+ (
+ text,
+ vec![
+ Point::new(4, 0)..Point::new(4, 3),
+ Point::new(5, 0)..Point::new(5, 3),
+ ],
+ ),
+ ],
+ cx,
+ )
+ });
+ let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
+ let buffer_ids = buffer_snapshot
+ .excerpts()
+ .map(|(_, buffer_snapshot, _)| buffer_snapshot.remote_id())
+ .dedup()
+ .collect::<Vec<_>>();
+ assert_eq!(buffer_ids.len(), 3);
+ let buffer_id_1 = buffer_ids[0];
+ let buffer_id_2 = buffer_ids[1];
+ let buffer_id_3 = buffer_ids[2];
+
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ 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 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"
+ );
+ assert_eq!(
+ blocks_snapshot.buffer_rows(BlockRow(0)).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,
+ ]
+ );
+
+ let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
+ let excerpt_blocks_2 = writer.insert(vec![
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 0))),
+ height: 1,
+ render: Arc::new(|_| div().into_any()),
+ priority: 0,
+ },
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(2, 0))),
+ height: 1,
+ render: Arc::new(|_| div().into_any()),
+ priority: 0,
+ },
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(3, 0))),
+ height: 1,
+ render: Arc::new(|_| div().into_any()),
+ priority: 0,
+ },
+ ]);
+ let excerpt_blocks_3 = writer.insert(vec![
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(4, 0))),
+ height: 1,
+ render: Arc::new(|_| div().into_any()),
+ priority: 0,
+ },
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(5, 0))),
+ height: 1,
+ render: Arc::new(|_| div().into_any()),
+ priority: 0,
+ },
+ ]);
+
+ 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"
+ );
+ assert_eq!(
+ blocks_snapshot.buffer_rows(BlockRow(0)).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,
+ ]
+ );
+
+ let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
+ buffer.read_with(cx, |buffer, cx| {
+ writer.fold_buffer(buffer_id_1, buffer, cx);
+ });
+ let excerpt_blocks_1 = writer.insert(vec![BlockProperties {
+ style: BlockStyle::Fixed,
+ placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(0, 0))),
+ height: 1,
+ render: Arc::new(|_| div().into_any()),
+ priority: 0,
+ }]);
+ let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+ let blocks = blocks_snapshot
+ .blocks_in_range(0..u32::MAX)
+ .collect::<Vec<_>>();
+ for (_, block) in &blocks {
+ if let BlockId::Custom(custom_block_id) = block.id() {
+ assert!(
+ !excerpt_blocks_1.contains(&custom_block_id),
+ "Should have no blocks from the folded buffer"
+ );
+ assert!(
+ excerpt_blocks_2.contains(&custom_block_id)
+ || excerpt_blocks_3.contains(&custom_block_id),
+ "Should have only blocks from unfolded buffers"
+ );
+ }
+ }
+ assert_eq!(
+ 1,
+ blocks
+ .iter()
+ .filter(|(_, block)| matches!(block, Block::FoldedBuffer { .. }))
+ .count(),
+ "Should have one folded block, prodicing a header of the second buffer"
+ );
+ 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"
+ );
+ assert_eq!(
+ blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+ vec![
+ None,
+ 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,
+ ]
+ );
+
+ let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
+ buffer.read_with(cx, |buffer, cx| {
+ writer.fold_buffer(buffer_id_2, buffer, cx);
+ });
+ let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+ let blocks = blocks_snapshot
+ .blocks_in_range(0..u32::MAX)
+ .collect::<Vec<_>>();
+ for (_, block) in &blocks {
+ if let BlockId::Custom(custom_block_id) = block.id() {
+ assert!(
+ !excerpt_blocks_1.contains(&custom_block_id),
+ "Should have no blocks from the folded buffer_1"
+ );
+ assert!(
+ !excerpt_blocks_2.contains(&custom_block_id),
+ "Should have no blocks from the folded buffer_2"
+ );
+ assert!(
+ excerpt_blocks_3.contains(&custom_block_id),
+ "Should have only blocks from unfolded buffers"
+ );
+ }
+ }
+ assert_eq!(
+ 2,
+ blocks
+ .iter()
+ .filter(|(_, block)| matches!(block, Block::FoldedBuffer { .. }))
+ .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.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+ vec![
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ Some(4),
+ None,
+ None,
+ Some(5),
+ None,
+ None,
+ ]
+ );
+
+ let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
+ buffer.read_with(cx, |buffer, cx| {
+ writer.unfold_buffer(buffer_id_1, buffer, cx);
+ });
+ let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+ let blocks = blocks_snapshot
+ .blocks_in_range(0..u32::MAX)
+ .collect::<Vec<_>>();
+ for (_, block) in &blocks {
+ if let BlockId::Custom(custom_block_id) = block.id() {
+ assert!(
+ !excerpt_blocks_2.contains(&custom_block_id),
+ "Should have no blocks from the folded buffer_2"
+ );
+ assert!(
+ excerpt_blocks_1.contains(&custom_block_id)
+ || excerpt_blocks_3.contains(&custom_block_id),
+ "Should have only blocks from unfolded buffers"
+ );
+ }
+ }
+ assert_eq!(
+ 1,
+ blocks
+ .iter()
+ .filter(|(_, block)| matches!(block, Block::FoldedBuffer { .. }))
+ .count(),
+ "Should be back to a single folded buffer, producing a header for buffer_2"
+ );
+ assert_eq!(
+ blocks_snapshot.text(),
+ "\n\n\n\n111\n\n\n\n\n\n\n\n555\n\n\n666\n\n",
+ "Should have extra newline for 111 buffer, due to a new block added when it was folded"
+ );
+ assert_eq!(
+ blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+ vec![
+ None,
+ None,
+ None,
+ None,
+ Some(0),
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ Some(4),
+ None,
+ None,
+ Some(5),
+ None,
+ None,
+ ]
+ );
+
+ let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
+ buffer.read_with(cx, |buffer, cx| {
+ writer.fold_buffer(buffer_id_3, buffer, cx);
+ });
+ let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+ let blocks = blocks_snapshot
+ .blocks_in_range(0..u32::MAX)
+ .collect::<Vec<_>>();
+ for (_, block) in &blocks {
+ if let BlockId::Custom(custom_block_id) = block.id() {
+ assert!(
+ excerpt_blocks_1.contains(&custom_block_id),
+ "Should have no blocks from the folded buffer_1"
+ );
+ assert!(
+ !excerpt_blocks_2.contains(&custom_block_id),
+ "Should have only blocks from unfolded buffers"
+ );
+ assert!(
+ !excerpt_blocks_3.contains(&custom_block_id),
+ "Should have only blocks from unfolded buffers"
+ );
+ }
+ }
+
+ assert_eq!(
+ blocks_snapshot.text(),
+ "\n\n\n\n111\n\n\n\n\n",
+ "Should have a single, first buffer left after folding"
+ );
+ assert_eq!(
+ blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+ vec![
+ None,
+ None,
+ None,
+ None,
+ Some(0),
+ None,
+ None,
+ None,
+ None,
+ None,
+ ]
+ );
+ }
+
+ #[gpui::test]
+ fn test_basic_buffer_fold(cx: &mut gpui::TestAppContext) {
+ cx.update(init_test);
+
+ let text = "111";
+
+ let buffer = cx.update(|cx| {
+ MultiBuffer::build_multi([(text, vec![Point::new(0, 0)..Point::new(0, 3)])], cx)
+ });
+ let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
+ let buffer_ids = buffer_snapshot
+ .excerpts()
+ .map(|(_, buffer_snapshot, _)| buffer_snapshot.remote_id())
+ .dedup()
+ .collect::<Vec<_>>();
+ assert_eq!(buffer_ids.len(), 1);
+ let buffer_id = buffer_ids[0];
+
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ 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 blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+
+ assert_eq!(blocks_snapshot.text(), "\n\n\n111\n");
+
+ let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
+ buffer.read_with(cx, |buffer, cx| {
+ writer.fold_buffer(buffer_id, buffer, cx);
+ });
+ let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+ let blocks = blocks_snapshot
+ .blocks_in_range(0..u32::MAX)
+ .collect::<Vec<_>>();
+ assert_eq!(
+ 1,
+ blocks
+ .iter()
+ .filter(|(_, block)| {
+ match block {
+ Block::FoldedBuffer { prev_excerpt, .. } => {
+ assert!(prev_excerpt.is_none());
+ true
+ }
+ _ => false,
+ }
+ })
+ .count(),
+ "Should have one folded block, prodicing a header of the second buffer"
+ );
+ assert_eq!(blocks_snapshot.text(), "\n");
+ assert_eq!(
+ blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+ vec![None, None],
+ "When fully folded, should be no buffer rows"
+ );
}
#[gpui::test(iterations = 100)]
@@ -678,6 +678,7 @@ pub struct Editor {
next_scroll_position: NextScrollCursorCenterTopBottom,
addons: HashMap<TypeId, Box<dyn Addon>>,
registered_buffers: HashMap<BufferId, OpenLspBufferHandle>,
+ toggle_fold_multiple_buffers: Task<()>,
_scroll_cursor_center_top_bottom_task: Task<()>,
}
@@ -1325,6 +1326,7 @@ impl Editor {
addons: HashMap::default(),
registered_buffers: HashMap::default(),
_scroll_cursor_center_top_bottom_task: Task::ready(()),
+ toggle_fold_multiple_buffers: Task::ready(()),
text_style_refinement: None,
};
this.tasks_update_task = Some(this.refresh_runnables(cx));
@@ -10311,22 +10313,53 @@ impl Editor {
}
pub fn toggle_fold(&mut self, _: &actions::ToggleFold, cx: &mut ViewContext<Self>) {
- let selection = self.selections.newest::<Point>(cx);
+ if self.is_singleton(cx) {
+ let selection = self.selections.newest::<Point>(cx);
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let range = if selection.is_empty() {
- let point = selection.head().to_display_point(&display_map);
- let start = DisplayPoint::new(point.row(), 0).to_point(&display_map);
- let end = DisplayPoint::new(point.row(), display_map.line_len(point.row()))
- .to_point(&display_map);
- start..end
- } else {
- selection.range()
- };
- if display_map.folds_in_range(range).next().is_some() {
- self.unfold_lines(&Default::default(), cx)
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let range = if selection.is_empty() {
+ let point = selection.head().to_display_point(&display_map);
+ let start = DisplayPoint::new(point.row(), 0).to_point(&display_map);
+ let end = DisplayPoint::new(point.row(), display_map.line_len(point.row()))
+ .to_point(&display_map);
+ start..end
+ } else {
+ selection.range()
+ };
+ if display_map.folds_in_range(range).next().is_some() {
+ self.unfold_lines(&Default::default(), cx)
+ } else {
+ self.fold(&Default::default(), cx)
+ }
} else {
- self.fold(&Default::default(), cx)
+ let (display_snapshot, selections) = self.selections.all_adjusted_display(cx);
+ let mut toggled_buffers = HashSet::default();
+ for selection in selections {
+ if let Some(buffer_id) = display_snapshot
+ .display_point_to_anchor(selection.head(), Bias::Right)
+ .buffer_id
+ {
+ if toggled_buffers.insert(buffer_id) {
+ if self.buffer_folded(buffer_id, cx) {
+ self.unfold_buffer(buffer_id, cx);
+ } else {
+ self.fold_buffer(buffer_id, cx);
+ }
+ }
+ }
+ if let Some(buffer_id) = display_snapshot
+ .display_point_to_anchor(selection.tail(), Bias::Left)
+ .buffer_id
+ {
+ if toggled_buffers.insert(buffer_id) {
+ if self.buffer_folded(buffer_id, cx) {
+ self.unfold_buffer(buffer_id, cx);
+ } else {
+ self.fold_buffer(buffer_id, cx);
+ }
+ }
+ }
+ }
}
}
@@ -10355,44 +10388,68 @@ impl Editor {
}
pub fn fold(&mut self, _: &actions::Fold, cx: &mut ViewContext<Self>) {
- let mut to_fold = Vec::new();
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let selections = self.selections.all_adjusted(cx);
+ if self.is_singleton(cx) {
+ let mut to_fold = Vec::new();
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let selections = self.selections.all_adjusted(cx);
- for selection in selections {
- let range = selection.range().sorted();
- let buffer_start_row = range.start.row;
+ for selection in selections {
+ let range = selection.range().sorted();
+ let buffer_start_row = range.start.row;
+
+ if range.start.row != range.end.row {
+ let mut found = false;
+ let mut row = range.start.row;
+ while row <= range.end.row {
+ if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row))
+ {
+ found = true;
+ row = crease.range().end.row + 1;
+ to_fold.push(crease);
+ } else {
+ row += 1
+ }
+ }
+ if found {
+ continue;
+ }
+ }
- if range.start.row != range.end.row {
- let mut found = false;
- let mut row = range.start.row;
- while row <= range.end.row {
+ for row in (0..=range.start.row).rev() {
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
- found = true;
- row = crease.range().end.row + 1;
- to_fold.push(crease);
- } else {
- row += 1
+ if crease.range().end.row >= buffer_start_row {
+ to_fold.push(crease);
+ if row <= range.start.row {
+ break;
+ }
+ }
}
}
- if found {
- continue;
- }
}
- for row in (0..=range.start.row).rev() {
- if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
- if crease.range().end.row >= buffer_start_row {
- to_fold.push(crease);
- if row <= range.start.row {
- break;
- }
+ self.fold_creases(to_fold, true, cx);
+ } else {
+ let (display_snapshot, selections) = self.selections.all_adjusted_display(cx);
+ let mut folded_buffers = HashSet::default();
+ for selection in selections {
+ if let Some(buffer_id) = display_snapshot
+ .display_point_to_anchor(selection.head(), Bias::Right)
+ .buffer_id
+ {
+ if folded_buffers.insert(buffer_id) {
+ self.fold_buffer(buffer_id, cx);
+ }
+ }
+ if let Some(buffer_id) = display_snapshot
+ .display_point_to_anchor(selection.tail(), Bias::Left)
+ .buffer_id
+ {
+ if folded_buffers.insert(buffer_id) {
+ self.fold_buffer(buffer_id, cx);
}
}
}
}
-
- self.fold_creases(to_fold, true, cx);
}
fn fold_at_level(&mut self, fold_at: &FoldAtLevel, cx: &mut ViewContext<Self>) {
@@ -10432,22 +10489,30 @@ impl Editor {
}
pub fn fold_all(&mut self, _: &actions::FoldAll, cx: &mut ViewContext<Self>) {
- if !self.buffer.read(cx).is_singleton() {
- return;
- }
-
- let mut fold_ranges = Vec::new();
- let snapshot = self.buffer.read(cx).snapshot(cx);
+ if self.buffer.read(cx).is_singleton() {
+ let mut fold_ranges = Vec::new();
+ let snapshot = self.buffer.read(cx).snapshot(cx);
- for row in 0..snapshot.max_row().0 {
- if let Some(foldable_range) =
- self.snapshot(cx).crease_for_buffer_row(MultiBufferRow(row))
- {
- fold_ranges.push(foldable_range);
+ for row in 0..snapshot.max_row().0 {
+ if let Some(foldable_range) =
+ self.snapshot(cx).crease_for_buffer_row(MultiBufferRow(row))
+ {
+ fold_ranges.push(foldable_range);
+ }
}
- }
- self.fold_creases(fold_ranges, true, cx);
+ self.fold_creases(fold_ranges, true, cx);
+ } else {
+ self.toggle_fold_multiple_buffers = cx.spawn(|editor, mut cx| async move {
+ editor
+ .update(&mut cx, |editor, cx| {
+ for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() {
+ editor.fold_buffer(buffer_id, cx);
+ }
+ })
+ .ok();
+ });
+ }
}
pub fn fold_function_bodies(
@@ -10519,22 +10584,45 @@ impl Editor {
}
pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext<Self>) {
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let buffer = &display_map.buffer_snapshot;
- let selections = self.selections.all::<Point>(cx);
- let ranges = selections
- .iter()
- .map(|s| {
- let range = s.display_range(&display_map).sorted();
- let mut start = range.start.to_point(&display_map);
- let mut end = range.end.to_point(&display_map);
- start.column = 0;
- end.column = buffer.line_len(MultiBufferRow(end.row));
- start..end
- })
- .collect::<Vec<_>>();
+ if self.is_singleton(cx) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let buffer = &display_map.buffer_snapshot;
+ let selections = self.selections.all::<Point>(cx);
+ let ranges = selections
+ .iter()
+ .map(|s| {
+ let range = s.display_range(&display_map).sorted();
+ let mut start = range.start.to_point(&display_map);
+ let mut end = range.end.to_point(&display_map);
+ start.column = 0;
+ end.column = buffer.line_len(MultiBufferRow(end.row));
+ start..end
+ })
+ .collect::<Vec<_>>();
- self.unfold_ranges(&ranges, true, true, cx);
+ self.unfold_ranges(&ranges, true, true, cx);
+ } else {
+ let (display_snapshot, selections) = self.selections.all_adjusted_display(cx);
+ let mut unfolded_buffers = HashSet::default();
+ for selection in selections {
+ if let Some(buffer_id) = display_snapshot
+ .display_point_to_anchor(selection.head(), Bias::Right)
+ .buffer_id
+ {
+ if unfolded_buffers.insert(buffer_id) {
+ self.unfold_buffer(buffer_id, cx);
+ }
+ }
+ if let Some(buffer_id) = display_snapshot
+ .display_point_to_anchor(selection.tail(), Bias::Left)
+ .buffer_id
+ {
+ if unfolded_buffers.insert(buffer_id) {
+ self.unfold_buffer(buffer_id, cx);
+ }
+ }
+ }
+ }
}
pub fn unfold_recursive(&mut self, _: &UnfoldRecursive, cx: &mut ViewContext<Self>) {
@@ -10574,8 +10662,20 @@ impl Editor {
}
pub fn unfold_all(&mut self, _: &actions::UnfoldAll, cx: &mut ViewContext<Self>) {
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx);
+ if self.buffer.read(cx).is_singleton() {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx);
+ } else {
+ self.toggle_fold_multiple_buffers = cx.spawn(|editor, mut cx| async move {
+ editor
+ .update(&mut cx, |editor, cx| {
+ for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() {
+ editor.unfold_buffer(buffer_id, cx);
+ }
+ })
+ .ok();
+ });
+ }
}
pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
@@ -10662,6 +10762,45 @@ impl Editor {
});
}
+ pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext<Self>) {
+ if self.buffer().read(cx).is_singleton() || self.buffer_folded(buffer_id, cx) {
+ return;
+ }
+ let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
+ return;
+ };
+ let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(&buffer, cx);
+ self.display_map
+ .update(cx, |display_map, cx| display_map.fold_buffer(buffer_id, cx));
+ cx.emit(EditorEvent::BufferFoldToggled {
+ ids: folded_excerpts.iter().map(|&(id, _)| id).collect(),
+ folded: true,
+ });
+ cx.notify();
+ }
+
+ pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext<Self>) {
+ if self.buffer().read(cx).is_singleton() || !self.buffer_folded(buffer_id, cx) {
+ return;
+ }
+ let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
+ return;
+ };
+ let unfolded_excerpts = self.buffer().read(cx).excerpts_for_buffer(&buffer, cx);
+ self.display_map.update(cx, |display_map, cx| {
+ display_map.unfold_buffer(buffer_id, cx);
+ });
+ cx.emit(EditorEvent::BufferFoldToggled {
+ ids: unfolded_excerpts.iter().map(|&(id, _)| id).collect(),
+ folded: false,
+ });
+ cx.notify();
+ }
+
+ pub fn buffer_folded(&self, buffer: BufferId, cx: &AppContext) -> bool {
+ self.display_map.read(cx).buffer_folded(buffer)
+ }
+
/// Removes any folds with the given ranges.
pub fn remove_folds_with_type<T: ToOffset + Clone>(
&mut self,
@@ -13820,6 +13959,10 @@ pub enum EditorEvent {
ExcerptsRemoved {
ids: Vec<ExcerptId>,
},
+ BufferFoldToggled {
+ ids: Vec<ExcerptId>,
+ folded: bool,
+ },
ExcerptsEdited {
ids: Vec<ExcerptId>,
},
@@ -4064,7 +4064,7 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
let snapshot = editor.snapshot(cx);
let snapshot = &snapshot.buffer_snapshot;
let placement = BlockPlacement::Replace(
- snapshot.anchor_after(Point::new(1, 0))..snapshot.anchor_after(Point::new(3, 0)),
+ snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
);
editor.insert_blocks(
[BlockProperties {
@@ -13905,6 +13905,412 @@ async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
});
}
+#[gpui::test]
+async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
+ let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
+ let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "first.rs": sample_text_1,
+ "second.rs": sample_text_2,
+ "third.rs": sample_text_3,
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
+ let worktree = project.update(cx, |project, cx| {
+ let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
+ assert_eq!(worktrees.len(), 1);
+ worktrees.pop().unwrap()
+ });
+ let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
+
+ let buffer_1 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "first.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let buffer_2 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "second.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let buffer_3 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "third.rs"), cx)
+ })
+ .await
+ .unwrap();
+
+ let multi_buffer = cx.new_model(|cx| {
+ let mut multi_buffer = MultiBuffer::new(ReadWrite);
+ multi_buffer.push_excerpts(
+ buffer_1.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(3, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(5, 0)..Point::new(7, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(9, 0)..Point::new(10, 4),
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multi_buffer.push_excerpts(
+ buffer_2.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(3, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(5, 0)..Point::new(7, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(9, 0)..Point::new(10, 4),
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multi_buffer.push_excerpts(
+ buffer_3.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(3, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(5, 0)..Point::new(7, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(9, 0)..Point::new(10, 4),
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multi_buffer
+ });
+ let multi_buffer_editor = cx.new_view(|cx| {
+ Editor::new(
+ EditorMode::Full,
+ multi_buffer,
+ Some(project.clone()),
+ true,
+ cx,
+ )
+ });
+
+ let full_text = "\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n";
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ full_text,
+ );
+
+ multi_buffer_editor.update(cx, |editor, cx| {
+ editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
+ });
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
+ "After folding the first buffer, its text should not be displayed"
+ );
+
+ multi_buffer_editor.update(cx, |editor, cx| {
+ editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
+ });
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
+ "After folding the second buffer, its text should not be displayed"
+ );
+
+ multi_buffer_editor.update(cx, |editor, cx| {
+ editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
+ });
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\n\n\n\n",
+ "After folding the third buffer, its text should not be displayed"
+ );
+
+ // Emulate selection inside the fold logic, that should work
+ multi_buffer_editor.update(cx, |editor, cx| {
+ editor.snapshot(cx).next_line_boundary(Point::new(0, 4));
+ });
+
+ multi_buffer_editor.update(cx, |editor, cx| {
+ editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
+ });
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
+ "After unfolding the second buffer, its text should be displayed"
+ );
+
+ multi_buffer_editor.update(cx, |editor, cx| {
+ editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
+ });
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
+ "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
+ );
+
+ multi_buffer_editor.update(cx, |editor, cx| {
+ editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
+ });
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ full_text,
+ "After unfolding the all buffers, all original text should be displayed"
+ );
+}
+
+#[gpui::test]
+async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let sample_text_1 = "1111\n2222\n3333".to_string();
+ let sample_text_2 = "4444\n5555\n6666".to_string();
+ let sample_text_3 = "7777\n8888\n9999".to_string();
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "first.rs": sample_text_1,
+ "second.rs": sample_text_2,
+ "third.rs": sample_text_3,
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
+ let worktree = project.update(cx, |project, cx| {
+ let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
+ assert_eq!(worktrees.len(), 1);
+ worktrees.pop().unwrap()
+ });
+ let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
+
+ let buffer_1 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "first.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let buffer_2 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "second.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let buffer_3 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "third.rs"), cx)
+ })
+ .await
+ .unwrap();
+
+ let multi_buffer = cx.new_model(|cx| {
+ let mut multi_buffer = MultiBuffer::new(ReadWrite);
+ multi_buffer.push_excerpts(
+ buffer_1.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(3, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ multi_buffer.push_excerpts(
+ buffer_2.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(3, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ multi_buffer.push_excerpts(
+ buffer_3.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(3, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ multi_buffer
+ });
+
+ let multi_buffer_editor = cx.new_view(|cx| {
+ Editor::new(
+ EditorMode::Full,
+ multi_buffer,
+ Some(project.clone()),
+ true,
+ cx,
+ )
+ });
+
+ let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ full_text,
+ );
+
+ multi_buffer_editor.update(cx, |editor, cx| {
+ editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
+ });
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
+ "After folding the first buffer, its text should not be displayed"
+ );
+
+ multi_buffer_editor.update(cx, |editor, cx| {
+ editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
+ });
+
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\n\n\n\n\n\n7777\n8888\n9999\n",
+ "After folding the second buffer, its text should not be displayed"
+ );
+
+ multi_buffer_editor.update(cx, |editor, cx| {
+ editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
+ });
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\n\n\n\n",
+ "After folding the third buffer, its text should not be displayed"
+ );
+
+ multi_buffer_editor.update(cx, |editor, cx| {
+ editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
+ });
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\n\n\n\n4444\n5555\n6666\n\n\n",
+ "After unfolding the second buffer, its text should be displayed"
+ );
+
+ multi_buffer_editor.update(cx, |editor, cx| {
+ editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
+ });
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
+ "After unfolding the first buffer, its text should be displayed"
+ );
+
+ multi_buffer_editor.update(cx, |editor, cx| {
+ editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
+ });
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ full_text,
+ "After unfolding all buffers, all original text should be displayed"
+ );
+}
+
+#[gpui::test]
+async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": sample_text,
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
+ let worktree = project.update(cx, |project, cx| {
+ let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
+ assert_eq!(worktrees.len(), 1);
+ worktrees.pop().unwrap()
+ });
+ let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
+
+ let buffer_1 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "main.rs"), cx)
+ })
+ .await
+ .unwrap();
+
+ let multi_buffer = cx.new_model(|cx| {
+ let mut multi_buffer = MultiBuffer::new(ReadWrite);
+ multi_buffer.push_excerpts(
+ buffer_1.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)
+ ..Point::new(
+ sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
+ 0,
+ ),
+ primary: None,
+ }],
+ cx,
+ );
+ multi_buffer
+ });
+ let multi_buffer_editor = cx.new_view(|cx| {
+ Editor::new(
+ EditorMode::Full,
+ multi_buffer,
+ Some(project.clone()),
+ true,
+ cx,
+ )
+ });
+
+ let selection_range = Point::new(1, 0)..Point::new(2, 0);
+ multi_buffer_editor.update(cx, |editor, cx| {
+ enum TestHighlight {}
+ let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
+ let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
+ editor.highlight_text::<TestHighlight>(
+ vec![highlight_range.clone()],
+ HighlightStyle::color(Hsla::green()),
+ cx,
+ );
+ editor.change_selections(None, cx, |s| s.select_ranges(Some(highlight_range)));
+ });
+
+ let full_text = format!("\n\n\n{sample_text}\n");
+ assert_eq!(
+ multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
+ full_text,
+ );
+}
+
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point
@@ -27,18 +27,18 @@ use crate::{
};
use client::ParticipantIndex;
use collections::{BTreeMap, HashMap, HashSet};
+use file_icons::FileIcons;
use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
use gpui::{
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
- transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
- ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
- FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
- ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
- ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
- StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
- WeakView, WindowContext,
+ transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClickEvent,
+ ClipboardItem, ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element,
+ ElementInputHandler, Entity, FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla,
+ InteractiveElement, IntoElement, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent,
+ MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent,
+ ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription,
+ TextRun, TextStyleRefinement, View, ViewContext, WeakView, WindowContext,
};
-use gpui::{ClickEvent, Subscription};
use itertools::Itertools;
use language::{
language_settings::{
@@ -49,8 +49,8 @@ use language::{
};
use lsp::DiagnosticSeverity;
use multi_buffer::{
- Anchor, AnchorRangeExt, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
- MultiBufferSnapshot, ToOffset,
+ Anchor, AnchorRangeExt, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint,
+ MultiBufferRow, MultiBufferSnapshot, ToOffset,
};
use project::{
project_settings::{GitGutterSetting, ProjectSettings},
@@ -1713,6 +1713,15 @@ impl EditorElement {
}
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
+ let buffer_folded = snapshot
+ .buffer_snapshot
+ .buffer_line_for_row(multibuffer_row)
+ .map(|(buffer_snapshot, _)| buffer_snapshot.remote_id())
+ .map(|buffer_id| editor.buffer_folded(buffer_id, cx))
+ .unwrap_or(false);
+ if buffer_folded {
+ return None;
+ }
if snapshot.is_line_folded(multibuffer_row) {
// Skip folded indicators, unless it's the starting line of a fold.
@@ -2087,6 +2096,7 @@ impl EditorElement {
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
cx: &mut WindowContext,
) -> (AnyElement, Size<Pixels>) {
+ let header_padding = px(6.0);
let mut element = match block {
Block::Custom(block) => {
let block_start = block.start().to_point(&snapshot.buffer_snapshot);
@@ -2136,21 +2146,58 @@ impl EditorElement {
.into_any()
}
+ Block::FoldedBuffer {
+ first_excerpt,
+ prev_excerpt,
+ show_excerpt_controls,
+ height,
+ ..
+ } => {
+ let icon_offset = gutter_dimensions.width
+ - (gutter_dimensions.left_padding + gutter_dimensions.margin);
+
+ let mut result = v_flex().id(block_id).w_full();
+ if let Some(prev_excerpt) = prev_excerpt {
+ if *show_excerpt_controls {
+ result = result.child(
+ h_flex()
+ .w(icon_offset)
+ .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height())
+ .flex_none()
+ .justify_end()
+ .child(self.render_expand_excerpt_button(
+ prev_excerpt.id,
+ ExpandExcerptDirection::Down,
+ IconName::ArrowDownFromLine,
+ cx,
+ )),
+ );
+ }
+ }
+
+ let jump_data = jump_data(snapshot, block_row_start, *height, first_excerpt, cx);
+ result
+ .child(self.render_buffer_header(
+ first_excerpt,
+ header_padding,
+ true,
+ jump_data,
+ cx,
+ ))
+ .into_any_element()
+ }
Block::ExcerptBoundary {
prev_excerpt,
next_excerpt,
show_excerpt_controls,
- starts_new_buffer,
height,
+ starts_new_buffer,
..
} => {
let icon_offset = gutter_dimensions.width
- (gutter_dimensions.left_padding + gutter_dimensions.margin);
- let header_padding = px(6.0);
-
let mut result = v_flex().id(block_id).w_full();
-
if let Some(prev_excerpt) = prev_excerpt {
if *show_excerpt_controls {
result = result.child(
@@ -2170,115 +2217,15 @@ impl EditorElement {
}
if let Some(next_excerpt) = next_excerpt {
- let buffer = &next_excerpt.buffer;
- let range = &next_excerpt.range;
- let jump_data = {
- let jump_path =
- project::File::from_dyn(buffer.file()).map(|file| ProjectPath {
- worktree_id: file.worktree_id(cx),
- path: file.path.clone(),
- });
- let jump_anchor = range
- .primary
- .as_ref()
- .map_or(range.context.start, |primary| primary.start);
-
- let excerpt_start = range.context.start;
- let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
- let offset_from_excerpt_start = if jump_anchor == excerpt_start {
- 0
- } else {
- let excerpt_start_row =
- language::ToPoint::to_point(&jump_anchor, buffer).row;
- jump_position.row - excerpt_start_row
- };
- let line_offset_from_top =
- block_row_start.0 + *height + offset_from_excerpt_start
- - snapshot
- .scroll_anchor
- .scroll_position(&snapshot.display_snapshot)
- .y as u32;
- JumpData {
- excerpt_id: next_excerpt.id,
- anchor: jump_anchor,
- position: language::ToPoint::to_point(&jump_anchor, buffer),
- path: jump_path,
- line_offset_from_top,
- }
- };
-
+ let jump_data = jump_data(snapshot, block_row_start, *height, next_excerpt, cx);
if *starts_new_buffer {
- let include_root = self
- .editor
- .read(cx)
- .project
- .as_ref()
- .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
- .unwrap_or_default();
- let path = buffer.resolve_file_path(cx, include_root);
- let filename = path
- .as_ref()
- .and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
- let parent_path = path.as_ref().and_then(|path| {
- Some(path.parent()?.to_string_lossy().to_string() + "/")
- });
-
- result = result.child(
- div()
- .px(header_padding)
- .pt(header_padding)
- .w_full()
- .h(FILE_HEADER_HEIGHT as f32 * cx.line_height())
- .child(
- h_flex()
- .id("path header block")
- .size_full()
- .flex_basis(Length::Definite(DefiniteLength::Fraction(
- 0.667,
- )))
- .px(gpui::px(12.))
- .rounded_md()
- .shadow_md()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().editor_subheader_background)
- .justify_between()
- .hover(|style| style.bg(cx.theme().colors().element_hover))
- .child(
- h_flex().gap_3().child(
- h_flex()
- .gap_2()
- .child(
- filename
- .map(SharedString::from)
- .unwrap_or_else(|| "untitled".into()),
- )
- .when_some(parent_path, |then, path| {
- then.child(div().child(path).text_color(
- cx.theme().colors().text_muted,
- ))
- }),
- ),
- )
- .child(Icon::new(IconName::ArrowUpRight))
- .cursor_pointer()
- .tooltip(|cx| {
- Tooltip::for_action("Jump to File", &OpenExcerpts, cx)
- })
- .on_mouse_down(MouseButton::Left, |_, cx| {
- cx.stop_propagation()
- })
- .on_click(cx.listener_for(&self.editor, {
- move |editor, e: &ClickEvent, cx| {
- editor.open_excerpts_common(
- Some(jump_data.clone()),
- e.down.modifiers.secondary(),
- cx,
- );
- }
- })),
- ),
- );
+ result = result.child(self.render_buffer_header(
+ next_excerpt,
+ header_padding,
+ false,
+ jump_data,
+ cx,
+ ));
if *show_excerpt_controls {
result = result.child(
h_flex()
@@ -2428,6 +2375,105 @@ impl EditorElement {
(element, final_size)
}
+ fn render_buffer_header(
+ &self,
+ for_excerpt: &ExcerptInfo,
+ header_padding: Pixels,
+ is_folded: bool,
+ jump_data: JumpData,
+ cx: &mut WindowContext,
+ ) -> Div {
+ let include_root = self
+ .editor
+ .read(cx)
+ .project
+ .as_ref()
+ .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
+ .unwrap_or_default();
+ let path = for_excerpt.buffer.resolve_file_path(cx, include_root);
+ let filename = path
+ .as_ref()
+ .and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
+ let parent_path = path
+ .as_ref()
+ .and_then(|path| Some(path.parent()?.to_string_lossy().to_string() + "/"));
+
+ div()
+ .px(header_padding)
+ .pt(header_padding)
+ .w_full()
+ .h(FILE_HEADER_HEIGHT as f32 * cx.line_height())
+ .child(
+ h_flex()
+ .id("path header block")
+ .size_full()
+ .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
+ .px(gpui::px(12.))
+ .rounded_md()
+ .shadow_md()
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .bg(cx.theme().colors().editor_subheader_background)
+ .justify_between()
+ .hover(|style| style.bg(cx.theme().colors().element_hover))
+ .child(
+ h_flex()
+ .gap_3()
+ .map(|header| {
+ let editor = self.editor.clone();
+ let buffer_id = for_excerpt.buffer_id;
+ let toggle_chevron_icon =
+ FileIcons::get_chevron_icon(!is_folded, cx)
+ .map(Icon::from_path);
+ header.child(
+ ButtonLike::new("toggle-buffer-fold")
+ .children(toggle_chevron_icon)
+ .on_click(move |_, cx| {
+ if is_folded {
+ editor.update(cx, |editor, cx| {
+ editor.unfold_buffer(buffer_id, cx);
+ });
+ } else {
+ editor.update(cx, |editor, cx| {
+ editor.fold_buffer(buffer_id, cx);
+ });
+ }
+ }),
+ )
+ })
+ .child(
+ h_flex()
+ .gap_2()
+ .child(
+ filename
+ .map(SharedString::from)
+ .unwrap_or_else(|| "untitled".into()),
+ )
+ .when_some(parent_path, |then, path| {
+ then.child(
+ div()
+ .child(path)
+ .text_color(cx.theme().colors().text_muted),
+ )
+ }),
+ ),
+ )
+ .child(Icon::new(IconName::ArrowUpRight))
+ .cursor_pointer()
+ .tooltip(|cx| Tooltip::for_action("Jump to File", &OpenExcerpts, cx))
+ .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
+ .on_click(cx.listener_for(&self.editor, {
+ move |editor, e: &ClickEvent, cx| {
+ editor.open_excerpts_common(
+ Some(jump_data.clone()),
+ e.down.modifiers.secondary(),
+ cx,
+ );
+ }
+ })),
+ )
+ }
+
fn render_expand_excerpt_button(
&self,
excerpt_id: ExcerptId,
@@ -4314,6 +4360,46 @@ impl EditorElement {
}
}
+fn jump_data(
+ snapshot: &EditorSnapshot,
+ block_row_start: DisplayRow,
+ height: u32,
+ for_excerpt: &ExcerptInfo,
+ cx: &mut WindowContext<'_>,
+) -> JumpData {
+ let range = &for_excerpt.range;
+ let buffer = &for_excerpt.buffer;
+ let jump_path = project::File::from_dyn(buffer.file()).map(|file| ProjectPath {
+ worktree_id: file.worktree_id(cx),
+ path: file.path.clone(),
+ });
+ let jump_anchor = range
+ .primary
+ .as_ref()
+ .map_or(range.context.start, |primary| primary.start);
+
+ let excerpt_start = range.context.start;
+ let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
+ let offset_from_excerpt_start = if jump_anchor == excerpt_start {
+ 0
+ } else {
+ let excerpt_start_row = language::ToPoint::to_point(&jump_anchor, buffer).row;
+ jump_position.row - excerpt_start_row
+ };
+ let line_offset_from_top = block_row_start.0 + height + offset_from_excerpt_start
+ - snapshot
+ .scroll_anchor
+ .scroll_position(&snapshot.display_snapshot)
+ .y as u32;
+ JumpData {
+ excerpt_id: for_excerpt.id,
+ anchor: jump_anchor,
+ position: language::ToPoint::to_point(&jump_anchor, buffer),
+ path: jump_path,
+ line_offset_from_top,
+ }
+}
+
fn inline_completion_popover_text(
editor_snapshot: &EditorSnapshot,
edits: &Vec<(Range<Anchor>, String)>,
@@ -5757,29 +5843,33 @@ impl Element for EditorElement {
if !expanded_add_hunks_by_rows
.contains_key(&newest_selection_display_row)
{
- let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
- MultiBufferRow(newest_selection_point.row),
- );
- if let Some((buffer, range)) = buffer {
- let buffer_id = buffer.remote_id();
- let row = range.start.row;
- let has_test_indicator = self
- .editor
- .read(cx)
- .tasks
- .contains_key(&(buffer_id, row));
-
- if !has_test_indicator {
- code_actions_indicator = self
- .layout_code_actions_indicator(
- line_height,
- newest_selection_head,
- scroll_pixel_position,
- &gutter_dimensions,
- &gutter_hitbox,
- &rows_with_hunk_bounds,
- cx,
- );
+ if !snapshot
+ .is_line_folded(MultiBufferRow(newest_selection_point.row))
+ {
+ let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
+ MultiBufferRow(newest_selection_point.row),
+ );
+ if let Some((buffer, range)) = buffer {
+ let buffer_id = buffer.remote_id();
+ let row = range.start.row;
+ let has_test_indicator = self
+ .editor
+ .read(cx)
+ .tasks
+ .contains_key(&(buffer_id, row));
+
+ if !has_test_indicator {
+ code_actions_indicator = self
+ .layout_code_actions_indicator(
+ line_height,
+ newest_selection_head,
+ scroll_pixel_position,
+ &gutter_dimensions,
+ &gutter_hitbox,
+ &rows_with_hunk_bounds,
+ cx,
+ );
+ }
}
}
}
@@ -172,13 +172,7 @@ pub fn indent_guides_in_range(
let start =
MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1));
// Filter out indent guides that are inside a fold
- let is_folded = snapshot.is_line_folded(start);
- let line_indent = snapshot.line_indent_for_buffer_row(start);
-
- let contained_in_fold =
- line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
-
- !(is_folded && contained_in_fold)
+ !snapshot.is_line_folded(start)
})
.collect()
}
@@ -195,6 +195,7 @@ pub struct ExcerptInfo {
pub buffer: BufferSnapshot,
pub buffer_id: BufferId,
pub range: ExcerptRange<text::Anchor>,
+ pub text_summary: TextSummary,
}
impl std::fmt::Debug for ExcerptInfo {
@@ -1546,6 +1547,33 @@ impl MultiBuffer {
excerpts
}
+ pub fn excerpt_ranges_for_buffer(
+ &self,
+ buffer_id: BufferId,
+ cx: &AppContext,
+ ) -> Vec<Range<Point>> {
+ let snapshot = self.read(cx);
+ let buffers = self.buffers.borrow();
+ let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, Point)>(&());
+ buffers
+ .get(&buffer_id)
+ .into_iter()
+ .flat_map(|state| &state.excerpts)
+ .filter_map(move |locator| {
+ cursor.seek_forward(&Some(locator), Bias::Left, &());
+ cursor.item().and_then(|excerpt| {
+ if excerpt.locator == *locator {
+ let excerpt_start = cursor.start().1;
+ let excerpt_end = excerpt_start + excerpt.text_summary.lines;
+ Some(excerpt_start..excerpt_end)
+ } else {
+ None
+ }
+ })
+ })
+ .collect()
+ }
+
pub fn excerpt_buffer_ids(&self) -> Vec<BufferId> {
self.snapshot
.borrow()
@@ -3559,6 +3587,7 @@ impl MultiBufferSnapshot {
buffer: excerpt.buffer.clone(),
buffer_id: excerpt.buffer_id,
range: excerpt.range.clone(),
+ text_summary: excerpt.text_summary.clone(),
});
if next.is_none() {
@@ -3574,6 +3603,7 @@ impl MultiBufferSnapshot {
buffer: prev_excerpt.buffer.clone(),
buffer_id: prev_excerpt.buffer_id,
range: prev_excerpt.range.clone(),
+ text_summary: prev_excerpt.text_summary.clone(),
});
let row = MultiBufferRow(cursor.start().1.row);
@@ -103,6 +103,7 @@ pub struct OutlinePanel {
active_item: Option<ActiveItem>,
_subscriptions: Vec<Subscription>,
updating_fs_entries: bool,
+ new_entries_for_fs_update: HashSet<ExcerptId>,
fs_entries_update_task: Task<()>,
cached_entries_update_task: Task<()>,
reveal_selection_task: Task<anyhow::Result<()>>,
@@ -116,6 +117,7 @@ pub struct OutlinePanel {
horizontal_scrollbar_state: ScrollbarState,
hide_scrollbar_task: Option<Task<()>>,
max_width_item_index: Option<usize>,
+ preserve_selection_on_buffer_fold_toggles: HashSet<BufferId>,
}
#[derive(Debug)]
@@ -716,6 +718,8 @@ impl OutlinePanel {
active_item: None,
pending_serialization: Task::ready(None),
updating_fs_entries: false,
+ new_entries_for_fs_update: HashSet::default(),
+ preserve_selection_on_buffer_fold_toggles: HashSet::default(),
fs_entries_update_task: Task::ready(()),
cached_entries_update_task: Task::ready(()),
reveal_selection_task: Task::ready(Ok(())),
@@ -811,7 +815,8 @@ impl OutlinePanel {
if self.filter_editor.focus_handle(cx).is_focused(cx) {
cx.propagate()
} else if let Some(selected_entry) = self.selected_entry().cloned() {
- self.open_entry(&selected_entry, true, false, cx);
+ self.toggle_expanded(&selected_entry, cx);
+ self.scroll_editor_to_entry(&selected_entry, true, false, cx);
}
}
@@ -834,7 +839,7 @@ impl OutlinePanel {
} else if let Some((active_editor, selected_entry)) =
self.active_editor().zip(self.selected_entry().cloned())
{
- self.open_entry(&selected_entry, true, true, cx);
+ self.scroll_editor_to_entry(&selected_entry, true, true, cx);
active_editor.update(cx, |editor, cx| editor.open_excerpts(action, cx));
}
}
@@ -849,12 +854,12 @@ impl OutlinePanel {
} else if let Some((active_editor, selected_entry)) =
self.active_editor().zip(self.selected_entry().cloned())
{
- self.open_entry(&selected_entry, true, true, cx);
+ self.scroll_editor_to_entry(&selected_entry, true, true, cx);
active_editor.update(cx, |editor, cx| editor.open_excerpts_in_split(action, cx));
}
}
- fn open_entry(
+ fn scroll_editor_to_entry(
&mut self,
entry: &PanelEntry,
prefer_selection_change: bool,
@@ -866,18 +871,14 @@ impl OutlinePanel {
};
let active_multi_buffer = active_editor.read(cx).buffer().clone();
let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
- let offset_from_top = if active_multi_buffer.read(cx).is_singleton() {
- Point::default()
- } else {
- Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32))
- };
-
let mut change_selection = prefer_selection_change;
+ let mut scroll_to_buffer = None;
let scroll_target = match entry {
PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => None,
PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
change_selection = false;
- let scroll_target = multi_buffer_snapshot.excerpts().find_map(
+ scroll_to_buffer = Some(*buffer_id);
+ multi_buffer_snapshot.excerpts().find_map(
|(excerpt_id, buffer_snapshot, excerpt_range)| {
if &buffer_snapshot.remote_id() == buffer_id {
multi_buffer_snapshot
@@ -886,13 +887,12 @@ impl OutlinePanel {
None
}
},
- );
- Some(offset_from_top).zip(scroll_target)
+ )
}
- PanelEntry::Fs(FsEntry::File(_, file_entry, ..)) => {
+ PanelEntry::Fs(FsEntry::File(_, file_entry, buffer_id, _)) => {
change_selection = false;
- let scroll_target = self
- .project
+ scroll_to_buffer = Some(*buffer_id);
+ self.project
.update(cx, |project, cx| {
project
.path_for_entry(file_entry.id, cx)
@@ -907,28 +907,23 @@ impl OutlinePanel {
let (excerpt_id, excerpt_range) = excerpts.first()?;
multi_buffer_snapshot
.anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
- });
- Some(offset_from_top).zip(scroll_target)
+ })
}
PanelEntry::Outline(OutlineEntry::Outline(_, excerpt_id, outline)) => {
- let scroll_target = multi_buffer_snapshot
+ multi_buffer_snapshot
.anchor_in_excerpt(*excerpt_id, outline.range.start)
.or_else(|| {
multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, outline.range.end)
- });
- Some(Point::default()).zip(scroll_target)
+ })
}
PanelEntry::Outline(OutlineEntry::Excerpt(_, excerpt_id, excerpt_range)) => {
- let scroll_target = multi_buffer_snapshot
- .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start);
- Some(Point::default()).zip(scroll_target)
- }
- PanelEntry::Search(SearchEntry { match_range, .. }) => {
- Some((Point::default(), match_range.start))
+ change_selection = false;
+ multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
}
+ PanelEntry::Search(SearchEntry { match_range, .. }) => Some(match_range.start),
};
- if let Some((offset, anchor)) = scroll_target {
+ if let Some(anchor) = scroll_target {
let activate = self
.workspace
.update(cx, |workspace, cx| match self.active_item() {
@@ -949,6 +944,43 @@ impl OutlinePanel {
);
});
} else {
+ let mut offset = Point::default();
+ let show_excerpt_controls = active_editor
+ .read(cx)
+ .display_map
+ .read(cx)
+ .show_excerpt_controls();
+ let expand_excerpt_control_height = 1.0;
+ if let Some(buffer_id) = scroll_to_buffer {
+ let current_folded = active_editor.read(cx).buffer_folded(buffer_id, cx);
+ if current_folded {
+ if show_excerpt_controls {
+ let previous_buffer_id = self
+ .fs_entries
+ .iter()
+ .rev()
+ .filter_map(|entry| match entry {
+ FsEntry::File(_, _, buffer_id, _)
+ | FsEntry::ExternalFile(buffer_id, _) => Some(*buffer_id),
+ FsEntry::Directory(..) => None,
+ })
+ .skip_while(|id| *id != buffer_id)
+ .skip(1)
+ .next();
+ if let Some(previous_buffer_id) = previous_buffer_id {
+ if !active_editor.read(cx).buffer_folded(previous_buffer_id, cx)
+ {
+ offset.y += expand_excerpt_control_height;
+ }
+ }
+ }
+ } else {
+ offset.y = -(active_editor.read(cx).file_header_size() as f32);
+ if show_excerpt_controls {
+ offset.y -= expand_excerpt_control_height;
+ }
+ }
+ }
active_editor.update(cx, |editor, cx| {
editor.set_scroll_anchor(ScrollAnchor { offset, anchor }, cx);
});
@@ -977,7 +1009,7 @@ impl OutlinePanel {
self.select_first(&SelectFirst {}, cx)
}
if let Some(selected_entry) = self.selected_entry().cloned() {
- self.open_entry(&selected_entry, true, false, cx);
+ self.scroll_editor_to_entry(&selected_entry, true, false, cx);
}
}
@@ -996,7 +1028,7 @@ impl OutlinePanel {
self.select_last(&SelectLast, cx)
}
if let Some(selected_entry) = self.selected_entry().cloned() {
- self.open_entry(&selected_entry, true, false, cx);
+ self.scroll_editor_to_entry(&selected_entry, true, false, cx);
}
}
@@ -1230,23 +1262,34 @@ impl OutlinePanel {
}
fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
- let entry_to_expand = match self.selected_entry() {
- Some(PanelEntry::FoldedDirs(worktree_id, dir_entries)) => dir_entries
- .last()
- .map(|entry| CollapsedEntry::Dir(*worktree_id, entry.id)),
- Some(PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry))) => {
+ let Some(active_editor) = self.active_editor() else {
+ return;
+ };
+ let Some(selected_entry) = self.selected_entry().cloned() else {
+ return;
+ };
+ let mut buffers_to_unfold = HashSet::default();
+ let entry_to_expand = match &selected_entry {
+ PanelEntry::FoldedDirs(worktree_id, dir_entries) => dir_entries.last().map(|entry| {
+ buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry));
+ CollapsedEntry::Dir(*worktree_id, entry.id)
+ }),
+ PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry)) => {
+ buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, dir_entry));
Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
}
- Some(PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _))) => {
+ PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
+ buffers_to_unfold.insert(*buffer_id);
Some(CollapsedEntry::File(*worktree_id, *buffer_id))
}
- Some(PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _))) => {
+ PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
+ buffers_to_unfold.insert(*buffer_id);
Some(CollapsedEntry::ExternalFile(*buffer_id))
}
- Some(PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _))) => {
+ PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => {
Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
}
- None | Some(PanelEntry::Search(_)) | Some(PanelEntry::Outline(..)) => None,
+ PanelEntry::Search(_) | PanelEntry::Outline(..) => return,
};
let Some(collapsed_entry) = entry_to_expand else {
return;
@@ -1254,70 +1297,120 @@ impl OutlinePanel {
let expanded = self.collapsed_entries.remove(&collapsed_entry);
if expanded {
if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
- self.project.update(cx, |project, cx| {
- project.expand_entry(worktree_id, dir_entry_id, cx);
+ let task = self.project.update(cx, |project, cx| {
+ project.expand_entry(worktree_id, dir_entry_id, cx)
});
+ if let Some(task) = task {
+ task.detach_and_log_err(cx);
+ }
+ };
+
+ active_editor.update(cx, |editor, cx| {
+ buffers_to_unfold.retain(|buffer_id| editor.buffer_folded(*buffer_id, cx));
+ });
+ self.select_entry(selected_entry, true, cx);
+ if buffers_to_unfold.is_empty() {
+ self.update_cached_entries(None, cx);
+ } else {
+ self.toggle_buffers_fold(buffers_to_unfold, false, cx)
+ .detach();
}
- self.update_cached_entries(None, cx);
} else {
self.select_next(&SelectNext, cx)
}
}
fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
+ let Some(active_editor) = self.active_editor() else {
+ return;
+ };
let Some(selected_entry) = self.selected_entry().cloned() else {
return;
};
- match &selected_entry {
+
+ let mut buffers_to_fold = HashSet::default();
+ let collapsed = match &selected_entry {
PanelEntry::Fs(FsEntry::Directory(worktree_id, selected_dir_entry)) => {
- self.collapsed_entries
- .insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id));
- self.select_entry(selected_entry, true, cx);
- self.update_cached_entries(None, cx);
+ if self
+ .collapsed_entries
+ .insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id))
+ {
+ buffers_to_fold
+ .extend(self.buffers_inside_directory(*worktree_id, selected_dir_entry));
+ true
+ } else {
+ false
+ }
}
PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
- self.collapsed_entries
- .insert(CollapsedEntry::File(*worktree_id, *buffer_id));
- self.select_entry(selected_entry, true, cx);
- self.update_cached_entries(None, cx);
+ if self
+ .collapsed_entries
+ .insert(CollapsedEntry::File(*worktree_id, *buffer_id))
+ {
+ buffers_to_fold.insert(*buffer_id);
+ true
+ } else {
+ false
+ }
}
PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
- self.collapsed_entries
- .insert(CollapsedEntry::ExternalFile(*buffer_id));
- self.select_entry(selected_entry, true, cx);
- self.update_cached_entries(None, cx);
+ if self
+ .collapsed_entries
+ .insert(CollapsedEntry::ExternalFile(*buffer_id))
+ {
+ buffers_to_fold.insert(*buffer_id);
+ true
+ } else {
+ false
+ }
}
PanelEntry::FoldedDirs(worktree_id, dir_entries) => {
+ let mut folded = false;
if let Some(dir_entry) = dir_entries.last() {
if self
.collapsed_entries
.insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
{
- self.select_entry(selected_entry, true, cx);
- self.update_cached_entries(None, cx);
+ folded = true;
+ buffers_to_fold
+ .extend(self.buffers_inside_directory(*worktree_id, dir_entry));
}
}
+ folded
}
- PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => {
- if self
- .collapsed_entries
- .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
- {
- self.select_entry(selected_entry, true, cx);
- self.update_cached_entries(None, cx);
- }
+ PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => self
+ .collapsed_entries
+ .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)),
+ PanelEntry::Search(_) | PanelEntry::Outline(..) => false,
+ };
+
+ if collapsed {
+ active_editor.update(cx, |editor, cx| {
+ buffers_to_fold.retain(|buffer_id| !editor.buffer_folded(*buffer_id, cx));
+ });
+ self.select_entry(selected_entry, true, cx);
+ if buffers_to_fold.is_empty() {
+ self.update_cached_entries(None, cx);
+ } else {
+ self.toggle_buffers_fold(buffers_to_fold, true, cx).detach();
}
- PanelEntry::Search(_) | PanelEntry::Outline(..) => {}
+ } else {
+ self.select_parent(&SelectParent, cx);
}
}
pub fn expand_all_entries(&mut self, _: &ExpandAllEntries, cx: &mut ViewContext<Self>) {
+ let Some(active_editor) = self.active_editor() else {
+ return;
+ };
+ let mut buffers_to_unfold = HashSet::default();
let expanded_entries =
self.fs_entries
.iter()
.fold(HashSet::default(), |mut entries, fs_entry| {
match fs_entry {
FsEntry::ExternalFile(buffer_id, _) => {
+ buffers_to_unfold.insert(*buffer_id);
entries.insert(CollapsedEntry::ExternalFile(*buffer_id));
entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
|excerpts| {
@@ -1331,6 +1424,7 @@ impl OutlinePanel {
entries.insert(CollapsedEntry::Dir(*worktree_id, entry.id));
}
FsEntry::File(worktree_id, _, buffer_id, _) => {
+ buffers_to_unfold.insert(*buffer_id);
entries.insert(CollapsedEntry::File(*worktree_id, *buffer_id));
entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
|excerpts| {
@@ -1340,15 +1434,27 @@ impl OutlinePanel {
},
));
}
- }
+ };
entries
});
self.collapsed_entries
.retain(|entry| !expanded_entries.contains(entry));
- self.update_cached_entries(None, cx);
+ active_editor.update(cx, |editor, cx| {
+ buffers_to_unfold.retain(|buffer_id| editor.buffer_folded(*buffer_id, cx));
+ });
+ if buffers_to_unfold.is_empty() {
+ self.update_cached_entries(None, cx);
+ } else {
+ self.toggle_buffers_fold(buffers_to_unfold, false, cx)
+ .detach();
+ }
}
pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
+ let Some(active_editor) = self.active_editor() else {
+ return;
+ };
+ let mut buffers_to_fold = HashSet::default();
let new_entries = self
.cached_entries
.iter()
@@ -1357,9 +1463,11 @@ impl OutlinePanel {
Some(CollapsedEntry::Dir(*worktree_id, entry.id))
}
PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
+ buffers_to_fold.insert(*buffer_id);
Some(CollapsedEntry::File(*worktree_id, *buffer_id))
}
PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
+ buffers_to_fold.insert(*buffer_id);
Some(CollapsedEntry::ExternalFile(*buffer_id))
}
PanelEntry::FoldedDirs(worktree_id, entries) => {
@@ -1372,14 +1480,28 @@ impl OutlinePanel {
})
.collect::<Vec<_>>();
self.collapsed_entries.extend(new_entries);
- self.update_cached_entries(None, cx);
+
+ active_editor.update(cx, |editor, cx| {
+ buffers_to_fold.retain(|buffer_id| !editor.buffer_folded(*buffer_id, cx));
+ });
+ if buffers_to_fold.is_empty() {
+ self.update_cached_entries(None, cx);
+ } else {
+ self.toggle_buffers_fold(buffers_to_fold, true, cx).detach();
+ }
}
fn toggle_expanded(&mut self, entry: &PanelEntry, cx: &mut ViewContext<Self>) {
+ let Some(active_editor) = self.active_editor() else {
+ return;
+ };
+ let mut fold = false;
+ let mut buffers_to_toggle = HashSet::default();
match entry {
PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry)) => {
let entry_id = dir_entry.id;
let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
+ buffers_to_toggle.extend(self.buffers_inside_directory(*worktree_id, dir_entry));
if self.collapsed_entries.remove(&collapsed_entry) {
self.project
.update(cx, |project, cx| {
@@ -1389,23 +1511,31 @@ impl OutlinePanel {
.detach_and_log_err(cx);
} else {
self.collapsed_entries.insert(collapsed_entry);
+ fold = true;
}
}
PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
+ buffers_to_toggle.insert(*buffer_id);
if !self.collapsed_entries.remove(&collapsed_entry) {
self.collapsed_entries.insert(collapsed_entry);
+ fold = true;
}
}
PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
let collapsed_entry = CollapsedEntry::ExternalFile(*buffer_id);
+ buffers_to_toggle.insert(*buffer_id);
if !self.collapsed_entries.remove(&collapsed_entry) {
self.collapsed_entries.insert(collapsed_entry);
+ fold = true;
}
}
PanelEntry::FoldedDirs(worktree_id, dir_entries) => {
- if let Some(entry_id) = dir_entries.first().map(|entry| entry.id) {
+ if let Some(dir_entry) = dir_entries.first() {
+ let entry_id = dir_entry.id;
let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
+ buffers_to_toggle
+ .extend(self.buffers_inside_directory(*worktree_id, dir_entry));
if self.collapsed_entries.remove(&collapsed_entry) {
self.project
.update(cx, |project, cx| {
@@ -1415,6 +1545,7 @@ impl OutlinePanel {
.detach_and_log_err(cx);
} else {
self.collapsed_entries.insert(collapsed_entry);
+ fold = true;
}
}
}
@@ -1427,8 +1558,56 @@ impl OutlinePanel {
PanelEntry::Search(_) | PanelEntry::Outline(..) => return,
}
+ active_editor.update(cx, |editor, cx| {
+ buffers_to_toggle.retain(|buffer_id| {
+ let folded = editor.buffer_folded(*buffer_id, cx);
+ if fold {
+ !folded
+ } else {
+ folded
+ }
+ });
+ });
+
self.select_entry(entry.clone(), true, cx);
- self.update_cached_entries(None, cx);
+ if buffers_to_toggle.is_empty() {
+ self.update_cached_entries(None, cx);
+ } else {
+ self.toggle_buffers_fold(buffers_to_toggle, fold, cx)
+ .detach();
+ }
+ }
+
+ fn toggle_buffers_fold(
+ &self,
+ buffers: HashSet<BufferId>,
+ fold: bool,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<()> {
+ let Some(active_editor) = self.active_editor() else {
+ return Task::ready(());
+ };
+ cx.spawn(|outline_panel, mut cx| async move {
+ outline_panel
+ .update(&mut cx, |outline_panel, cx| {
+ active_editor.update(cx, |editor, cx| {
+ for buffer_id in buffers {
+ outline_panel
+ .preserve_selection_on_buffer_fold_toggles
+ .insert(buffer_id);
+ if fold {
+ editor.fold_buffer(buffer_id, cx);
+ } else {
+ editor.unfold_buffer(buffer_id, cx);
+ }
+ }
+ });
+ if let Some(selection) = outline_panel.selected_entry().cloned() {
+ outline_panel.scroll_editor_to_entry(&selection, false, false, cx);
+ }
+ })
+ .ok();
+ })
}
fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
@@ -1816,7 +1995,7 @@ impl OutlinePanel {
icon.unwrap_or_else(empty_icon),
)
}
- FsEntry::ExternalFile(buffer_id, ..) => {
+ FsEntry::ExternalFile(buffer_id, _) => {
let color = entry_label_color(is_active);
let (icon, name) = match self.buffer_snapshot_for_id(*buffer_id, cx) {
Some(buffer_snapshot) => match buffer_snapshot.file() {
@@ -2037,7 +2216,7 @@ impl OutlinePanel {
}
let change_focus = event.down.click_count > 1;
outline_panel.toggle_expanded(&clicked_entry, cx);
- outline_panel.open_entry(&clicked_entry, true, change_focus, cx);
+ outline_panel.scroll_editor_to_entry(&clicked_entry, true, change_focus, cx);
})
})
.cursor_pointer()
@@ -2107,8 +2286,7 @@ impl OutlinePanel {
fn update_fs_entries(
&mut self,
- active_editor: &View<Editor>,
- new_entries: HashSet<ExcerptId>,
+ active_editor: View<Editor>,
debounce: Option<Duration>,
cx: &mut ViewContext<Self>,
) {
@@ -2118,6 +2296,7 @@ impl OutlinePanel {
let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
let active_multi_buffer = active_editor.read(cx).buffer().clone();
+ let new_entries = self.new_entries_for_fs_update.clone();
self.updating_fs_entries = true;
self.fs_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
if let Some(debounce) = debounce {
@@ -2141,10 +2320,11 @@ impl OutlinePanel {
let worktree = file.map(|file| file.worktree.read(cx).snapshot());
let is_new = new_entries.contains(&excerpt_id)
|| !outline_panel.excerpts.contains_key(&buffer_id);
+ let is_folded = active_editor.read(cx).buffer_folded(buffer_id, cx);
buffer_excerpts
.entry(buffer_id)
- .or_insert_with(|| (is_new, Vec::new(), entry_id, worktree))
- .1
+ .or_insert_with(|| (is_new, is_folded, Vec::new(), entry_id, worktree))
+ .2
.push(excerpt_id);
let outlines = match outline_panel
@@ -2196,8 +2376,21 @@ impl OutlinePanel {
>::default();
let mut external_excerpts = HashMap::default();
- for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
- if is_new {
+ for (buffer_id, (is_new, is_folded, excerpts, entry_id, worktree)) in
+ buffer_excerpts
+ {
+ if is_folded {
+ match &worktree {
+ Some(worktree) => {
+ new_collapsed_entries
+ .insert(CollapsedEntry::File(worktree.id(), buffer_id));
+ }
+ None => {
+ new_collapsed_entries
+ .insert(CollapsedEntry::ExternalFile(buffer_id));
+ }
+ }
+ } else if is_new {
match &worktree {
Some(worktree) => {
new_collapsed_entries
@@ -2438,6 +2631,7 @@ impl OutlinePanel {
outline_panel
.update(&mut cx, |outline_panel, cx| {
outline_panel.updating_fs_entries = false;
+ outline_panel.new_entries_for_fs_update.clear();
outline_panel.excerpts = new_excerpts;
outline_panel.collapsed_entries = new_collapsed_entries;
outline_panel.unfolded_dirs = new_unfolded_dirs;
@@ -2475,10 +2669,10 @@ impl OutlinePanel {
item_handle: new_active_item.downgrade_item(),
active_editor: new_active_editor.downgrade(),
});
- let new_entries =
- HashSet::from_iter(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
+ self.new_entries_for_fs_update
+ .extend(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
self.selected_entry.invalidate();
- self.update_fs_entries(&new_active_editor, new_entries, None, cx);
+ self.update_fs_entries(new_active_editor, None, cx);
}
fn clear_previous(&mut self, cx: &mut WindowContext<'_>) {
@@ -2517,6 +2711,20 @@ impl OutlinePanel {
.read(cx)
.excerpt_containing(selection, cx)?;
let buffer_id = buffer.read(cx).remote_id();
+
+ if editor.read(cx).buffer_folded(buffer_id, cx) {
+ return self
+ .fs_entries
+ .iter()
+ .find(|fs_entry| match fs_entry {
+ FsEntry::Directory(..) => false,
+ FsEntry::File(_, _, file_buffer_id, _)
+ | FsEntry::ExternalFile(file_buffer_id, _) => *file_buffer_id == buffer_id,
+ })
+ .cloned()
+ .map(PanelEntry::Fs);
+ }
+
let selection_display_point = selection.to_display_point(&editor_snapshot);
match &self.mode {
@@ -2919,6 +3127,9 @@ impl OutlinePanel {
cx: &mut ViewContext<'_, Self>,
) -> Task<(Vec<CachedEntry>, Option<usize>)> {
let project = self.project.clone();
+ let Some(active_editor) = self.active_editor() else {
+ return Task::ready((Vec::new(), None));
+ };
cx.spawn(|outline_panel, mut cx| async move {
let mut generation_state = GenerationState::default();
@@ -3149,6 +3360,7 @@ impl OutlinePanel {
if is_singleton || query.is_some() || (should_add && is_expanded) {
outline_panel.add_search_entries(
&mut generation_state,
+ &active_editor,
entry.clone(),
depth,
query.clone(),
@@ -3173,16 +3385,18 @@ impl OutlinePanel {
None
};
if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
- outline_panel.add_excerpt_entries(
- &mut generation_state,
- buffer_id,
- entry_excerpts,
- depth,
- track_matches,
- is_singleton,
- query.as_deref(),
- cx,
- );
+ if !active_editor.read(cx).buffer_folded(buffer_id, cx) {
+ outline_panel.add_excerpt_entries(
+ &mut generation_state,
+ buffer_id,
+ entry_excerpts,
+ depth,
+ track_matches,
+ is_singleton,
+ query.as_deref(),
+ cx,
+ );
+ }
}
}
}
@@ -3536,15 +3750,13 @@ impl OutlinePanel {
fn add_search_entries(
&mut self,
state: &mut GenerationState,
+ active_editor: &View<Editor>,
parent_entry: FsEntry,
parent_depth: usize,
filter_query: Option<String>,
is_singleton: bool,
cx: &mut ViewContext<Self>,
) {
- if self.active_editor().is_none() {
- return;
- };
let ItemsDisplayMode::Search(search_state) = &mut self.mode else {
return;
};
@@ -3560,10 +3772,27 @@ impl OutlinePanel {
.collect::<HashSet<_>>();
let depth = if is_singleton { 0 } else { parent_depth + 1 };
- let new_search_matches = search_state.matches.iter().filter(|(match_range, _)| {
- related_excerpts.contains(&match_range.start.excerpt_id)
- || related_excerpts.contains(&match_range.end.excerpt_id)
- });
+ let new_search_matches = search_state
+ .matches
+ .iter()
+ .filter(|(match_range, _)| {
+ related_excerpts.contains(&match_range.start.excerpt_id)
+ || related_excerpts.contains(&match_range.end.excerpt_id)
+ })
+ .filter(|(match_range, _)| {
+ let editor = active_editor.read(cx);
+ if let Some(buffer_id) = match_range.start.buffer_id {
+ if editor.buffer_folded(buffer_id, cx) {
+ return false;
+ }
+ }
+ if let Some(buffer_id) = match_range.start.buffer_id {
+ if editor.buffer_folded(buffer_id, cx) {
+ return false;
+ }
+ }
+ true
+ });
let new_search_entries = new_search_matches
.map(|(match_range, search_data)| SearchEntry {
@@ -4071,6 +4300,41 @@ impl OutlinePanel {
),
)
}
+
+ fn buffers_inside_directory(
+ &self,
+ dir_worktree: WorktreeId,
+ dir_entry: &Entry,
+ ) -> HashSet<BufferId> {
+ if !dir_entry.is_dir() {
+ debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
+ return HashSet::default();
+ }
+
+ self.fs_entries
+ .iter()
+ .skip_while(|fs_entry| match fs_entry {
+ FsEntry::Directory(worktree_id, entry) => {
+ *worktree_id != dir_worktree || entry != dir_entry
+ }
+ _ => true,
+ })
+ .skip(1)
+ .take_while(|fs_entry| match fs_entry {
+ FsEntry::ExternalFile(..) => false,
+ FsEntry::Directory(worktree_id, entry) => {
+ *worktree_id == dir_worktree && entry.path.starts_with(&dir_entry.path)
+ }
+ FsEntry::File(worktree_id, entry, ..) => {
+ *worktree_id == dir_worktree && entry.path.starts_with(&dir_entry.path)
+ }
+ })
+ .filter_map(|fs_entry| match fs_entry {
+ FsEntry::File(_, _, buffer_id, _) => Some(*buffer_id),
+ _ => None,
+ })
+ .collect()
+ }
}
fn workspace_active_editor(
@@ -4192,12 +4456,7 @@ impl Panel for OutlinePanel {
if outline_panel.should_replace_active_item(active_item.as_ref()) {
outline_panel.replace_active_editor(active_item, active_editor, cx);
} else {
- outline_panel.update_fs_entries(
- &active_editor,
- HashSet::default(),
- None,
- cx,
- )
+ outline_panel.update_fs_entries(active_editor, None, cx)
}
} else if !outline_panel.pinned {
outline_panel.clear_previous(cx);
@@ -4350,12 +4609,10 @@ fn subscribe_for_editor_events(
cx.notify();
}
EditorEvent::ExcerptsAdded { excerpts, .. } => {
- outline_panel.update_fs_entries(
- &editor,
- excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(),
- debounce,
- cx,
- );
+ outline_panel
+ .new_entries_for_fs_update
+ .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
+ outline_panel.update_fs_entries(editor, debounce, cx);
}
EditorEvent::ExcerptsRemoved { ids } => {
let mut ids = ids.iter().collect::<HashSet<_>>();
@@ -4365,7 +4622,7 @@ fn subscribe_for_editor_events(
break;
}
}
- outline_panel.update_fs_entries(&editor, HashSet::default(), debounce, cx);
+ outline_panel.update_fs_entries(editor, debounce, cx);
}
EditorEvent::ExcerptsExpanded { ids } => {
outline_panel.invalidate_outlines(ids);
@@ -4375,6 +4632,73 @@ fn subscribe_for_editor_events(
outline_panel.invalidate_outlines(ids);
outline_panel.update_non_fs_items(cx);
}
+ EditorEvent::BufferFoldToggled { ids, .. } => {
+ outline_panel.invalidate_outlines(ids);
+ let mut latest_unfolded_buffer_id = None;
+ let mut latest_folded_buffer_id = None;
+ let mut ignore_selections_change = false;
+ outline_panel.new_entries_for_fs_update.extend(
+ ids.iter()
+ .filter(|id| {
+ outline_panel
+ .excerpts
+ .iter()
+ .find_map(|(buffer_id, excerpts)| {
+ if excerpts.contains_key(id) {
+ ignore_selections_change |= outline_panel
+ .preserve_selection_on_buffer_fold_toggles
+ .remove(buffer_id);
+ Some(buffer_id)
+ } else {
+ None
+ }
+ })
+ .map(|buffer_id| {
+ if editor.read(cx).buffer_folded(*buffer_id, cx) {
+ latest_folded_buffer_id = Some(*buffer_id);
+ false
+ } else {
+ latest_unfolded_buffer_id = Some(*buffer_id);
+ true
+ }
+ })
+ .unwrap_or(true)
+ })
+ .copied(),
+ );
+ if !ignore_selections_change {
+ if let Some(entry_to_select) = latest_unfolded_buffer_id
+ .or(latest_folded_buffer_id)
+ .and_then(|toggled_buffer_id| {
+ outline_panel
+ .fs_entries
+ .iter()
+ .find_map(|fs_entry| match fs_entry {
+ FsEntry::ExternalFile(buffer_id, _) => {
+ if *buffer_id == toggled_buffer_id {
+ Some(fs_entry.clone())
+ } else {
+ None
+ }
+ }
+ FsEntry::File(_, _, buffer_id, _) => {
+ if *buffer_id == toggled_buffer_id {
+ Some(fs_entry.clone())
+ } else {
+ None
+ }
+ }
+ FsEntry::Directory(..) => None,
+ })
+ })
+ .map(PanelEntry::Fs)
+ {
+ outline_panel.select_entry(entry_to_select, true, cx);
+ }
+ }
+
+ outline_panel.update_fs_entries(editor, debounce, cx);
+ }
EditorEvent::Reparsed(buffer_id) => {
if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
for (_, excerpt) in excerpts {
@@ -4531,6 +4855,8 @@ mod tests {
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
});
+ cx.executor()
+ .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
@@ -4563,6 +4889,8 @@ mod tests {
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.expand_all_entries(&ExpandAllEntries, cx);
});
+ cx.executor()
+ .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.select_parent(&SelectParent, cx);
@@ -4591,6 +4919,8 @@ mod tests {
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
});
+ cx.executor()
+ .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
@@ -4615,6 +4945,8 @@ mod tests {
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.expand_selected_entry(&ExpandSelectedEntry, cx);
});
+ cx.executor()
+ .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
@@ -5053,6 +5385,8 @@ mod tests {
}
outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
});
+ cx.executor()
+ .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
@@ -5072,6 +5406,91 @@ mod tests {
search: static"#
);
});
+
+ outline_panel.update(cx, |outline_panel, cx| {
+ // Move to the next visible non-FS entry
+ for _ in 0..3 {
+ outline_panel.select_next(&SelectNext, cx);
+ }
+ });
+ cx.run_until_parked();
+ outline_panel.update(cx, |outline_panel, cx| {
+ assert_eq!(
+ display_entries(
+ &snapshot(&outline_panel, cx),
+ &outline_panel.cached_entries,
+ outline_panel.selected_entry()
+ ),
+ r#"/
+ public/lottie/
+ syntax-tree.json
+ search: { "something": "static" }
+ src/
+ app/(site)/
+ components/
+ ErrorBoundary.tsx
+ search: static <==== selected"#
+ );
+ });
+
+ outline_panel.update(cx, |outline_panel, cx| {
+ outline_panel
+ .active_editor()
+ .expect("Should have an active editor")
+ .update(cx, |editor, cx| {
+ editor.toggle_fold(&editor::actions::ToggleFold, cx)
+ });
+ });
+ cx.executor()
+ .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+ cx.run_until_parked();
+ outline_panel.update(cx, |outline_panel, cx| {
+ assert_eq!(
+ display_entries(
+ &snapshot(&outline_panel, cx),
+ &outline_panel.cached_entries,
+ outline_panel.selected_entry()
+ ),
+ r#"/
+ public/lottie/
+ syntax-tree.json
+ search: { "something": "static" }
+ src/
+ app/(site)/
+ components/
+ ErrorBoundary.tsx <==== selected"#
+ );
+ });
+
+ outline_panel.update(cx, |outline_panel, cx| {
+ outline_panel
+ .active_editor()
+ .expect("Should have an active editor")
+ .update(cx, |editor, cx| {
+ editor.toggle_fold(&editor::actions::ToggleFold, cx)
+ });
+ });
+ cx.executor()
+ .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+ cx.run_until_parked();
+ outline_panel.update(cx, |outline_panel, cx| {
+ assert_eq!(
+ display_entries(
+ &snapshot(&outline_panel, cx),
+ &outline_panel.cached_entries,
+ outline_panel.selected_entry()
+ ),
+ r#"/
+ public/lottie/
+ syntax-tree.json
+ search: { "something": "static" }
+ src/
+ app/(site)/
+ components/
+ ErrorBoundary.tsx <==== selected
+ search: static"#
+ );
+ });
}
async fn add_outline_panel(