diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index e399fe08f31fe..ec9d86f3d526f 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -1050,6 +1050,7 @@ fn editor_blocks( .ok()? } + Block::FoldedBuffer { .. } => FILE_HEADER.into(), Block::ExcerptBoundary { starts_new_buffer, .. } => { diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 9f835d5653dad..76b508079d198 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -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) { + 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) { + 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>, @@ -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, diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index b495669ef8eb8..00c87c84cb2dd 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -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, } pub struct BlockMapReader<'a> { @@ -83,7 +85,7 @@ pub type RenderBlock = Arc AnyElement pub enum BlockPlacement { Above(T), Below(T), - Replace(Range), + Replace(RangeInclusive), } impl BlockPlacement { @@ -91,7 +93,7 @@ impl BlockPlacement { 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 BlockPlacement { 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 BlockPlacement { 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 BlockPlacement { 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_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 { 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 { 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 { - 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 { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - pub struct CustomBlock { id: CustomBlockId, placement: BlockPlacement, @@ -272,6 +240,7 @@ pub struct BlockContext<'a, 'b> { #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)] pub enum BlockId { ExcerptBoundary(Option), + FoldedBuffer(ExcerptId), Custom(CustomBlockId), } @@ -283,6 +252,7 @@ impl From 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), + FoldedBuffer { + first_excerpt: ExcerptInfo, + prev_excerpt: Option, + height: u32, + show_excerpt_controls: bool, + }, ExcerptBoundary { prev_excerpt: Option, next_excerpt: Option, @@ -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, range: R, - wrap_snapshot: &'c WrapSnapshot, - ) -> impl Iterator, Block)> + 'b + wrap_snapshot: &'a WrapSnapshot, + ) -> impl Iterator, Block)> + 'a where R: RangeBounds, 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, 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, @@ -1292,42 +1413,44 @@ impl BlockSnapshot { pub fn block_for_id(&self, block_id: BlockId) -> Option { 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::(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::(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::(&()); + 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::>(); + 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![ + 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![ + 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::>(); + 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![ + 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::>(); + 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![ + 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::>(); + 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![ + 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::>(); + 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![ + 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::>(); + 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::>(); + 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![None, None], + "When fully folded, should be no buffer rows" + ); } #[gpui::test(iterations = 100)] @@ -2423,18 +3018,13 @@ mod tests { rng.gen_range(offset..=buffer.len()), Bias::Left, )); - BlockPlacement::Replace(start..end) + BlockPlacement::Replace(start..=end) } 1 => BlockPlacement::Above(buffer.anchor_after(offset)), _ => BlockPlacement::Below(buffer.anchor_after(offset)), }; let height = rng.gen_range(min_height..5); - log::info!( - "inserting block {:?} with height {}", - placement.as_ref().map(|p| p.to_point(&buffer)), - height - ); BlockProperties { style: BlockStyle::Fixed, placement, @@ -2454,13 +3044,26 @@ mod tests { wrap_map.sync(tab_snapshot, tab_edits, cx) }); let mut block_map = block_map.write(wraps_snapshot, wrap_edits); - block_map.insert(block_properties.iter().map(|props| BlockProperties { - placement: props.placement.clone(), - height: props.height, - style: props.style, - render: Arc::new(|_| div().into_any()), - priority: 0, - })); + let block_ids = + block_map.insert(block_properties.iter().map(|props| BlockProperties { + placement: props.placement.clone(), + height: props.height, + style: props.style, + render: Arc::new(|_| div().into_any()), + priority: 0, + })); + + for (block_properties, block_id) in block_properties.iter().zip(block_ids) { + log::info!( + "inserted block {:?} with height {} and id {:?}", + block_properties + .placement + .as_ref() + .map(|p| p.to_point(&buffer_snapshot)), + block_properties.height, + block_id + ); + } } 40..=59 if !block_map.custom_blocks.is_empty() => { let block_count = rng.gen_range(1..=4.min(block_map.custom_blocks.len())); @@ -2479,8 +3082,92 @@ mod tests { wrap_map.sync(tab_snapshot, tab_edits, cx) }); let mut block_map = block_map.write(wraps_snapshot, wrap_edits); + log::info!( + "removing {} blocks: {:?}", + block_ids_to_remove.len(), + block_ids_to_remove + ); block_map.remove(block_ids_to_remove); } + 60..=79 => { + if buffer.read_with(cx, |buffer, _| buffer.is_singleton()) { + log::info!("Noop fold/unfold operation on a singleton buffer"); + continue; + } + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), vec![]); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tab_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { + wrap_map.sync(tab_snapshot, tab_edits, cx) + }); + let mut block_map = block_map.write(wraps_snapshot, wrap_edits); + let (unfolded_buffers, folded_buffers) = buffer.read_with(cx, |buffer, _| { + let folded_buffers = block_map + .0 + .folded_buffers + .iter() + .cloned() + .collect::>(); + let mut unfolded_buffers = buffer.excerpt_buffer_ids(); + unfolded_buffers.dedup(); + log::debug!("All buffers {unfolded_buffers:?}"); + log::debug!("Folded buffers {folded_buffers:?}"); + unfolded_buffers + .retain(|buffer_id| !block_map.0.folded_buffers.contains(buffer_id)); + (unfolded_buffers, folded_buffers) + }); + let mut folded_count = folded_buffers.len(); + let mut unfolded_count = unfolded_buffers.len(); + + let fold = !unfolded_buffers.is_empty() && rng.gen_bool(0.5); + let unfold = !folded_buffers.is_empty() && rng.gen_bool(0.5); + if !fold && !unfold { + log::info!("Noop fold/unfold operation. Unfolded buffers: {unfolded_count}, folded buffers: {folded_count}"); + continue; + } + + buffer.update(cx, |buffer, cx| { + if fold { + let buffer_to_fold = + unfolded_buffers[rng.gen_range(0..unfolded_buffers.len())]; + log::info!("Folding {buffer_to_fold:?}"); + let related_excerpts = buffer_snapshot + .excerpts() + .filter_map(|(excerpt_id, buffer, range)| { + if buffer.remote_id() == buffer_to_fold { + Some(( + excerpt_id, + buffer + .text_for_range(range.context) + .collect::(), + )) + } else { + None + } + }) + .collect::>(); + log::info!( + "Folding {buffer_to_fold:?}, related excerpts: {related_excerpts:?}" + ); + folded_count += 1; + unfolded_count -= 1; + block_map.fold_buffer(buffer_to_fold, buffer, cx); + } + if unfold { + let buffer_to_unfold = + folded_buffers[rng.gen_range(0..folded_buffers.len())]; + log::info!("Unfolding {buffer_to_unfold:?}"); + unfolded_count += 1; + folded_count -= 1; + block_map.unfold_buffer(buffer_to_unfold, buffer, cx); + } + log::info!( + "Unfolded buffers: {unfolded_count}, folded buffers: {folded_count}" + ); + }); + } _ => { buffer.update(cx, |buffer, cx| { let mutation_count = rng.gen_range(1..=5); @@ -2523,6 +3210,7 @@ mod tests { buffer_start_header_height, excerpt_header_height, &buffer_snapshot, + &block_map.folded_buffers, 0.., &wraps_snapshot, )); @@ -2590,14 +3278,19 @@ mod tests { if let Some((BlockPlacement::Replace(replace_range), block)) = sorted_blocks_iter.peek() { - if wrap_row >= replace_range.start.0 { + if wrap_row >= replace_range.start().0 { is_in_replace_block = true; - if wrap_row == replace_range.start.0 { - expected_buffer_rows.push(input_buffer_rows[multibuffer_row as usize]); + if wrap_row == replace_range.start().0 { + if matches!(block, Block::FoldedBuffer { .. }) { + expected_buffer_rows.push(None); + } else { + expected_buffer_rows + .push(input_buffer_rows[multibuffer_row as usize]); + } } - if wrap_row == replace_range.end.0 { + if wrap_row == replace_range.end().0 { expected_block_positions.push((block_row, block.id())); let text = "\n".repeat((block.height() - 1) as usize); if block_row > 0 { @@ -2654,13 +3347,18 @@ mod tests { let expected_lines = expected_text.split('\n').collect::>(); let expected_row_count = expected_lines.len(); + log::info!("expected text: {expected_text:?}"); assert_eq!( blocks_snapshot.max_point().row + 1, - expected_row_count as u32 + expected_row_count as u32, + "actual row count != expected row count", + ); + assert_eq!( + blocks_snapshot.text(), + expected_text, + "actual text != expected text", ); - - log::info!("expected text: {:?}", expected_text); for start_row in 0..expected_row_count { let end_row = rng.gen_range(start_row + 1..=expected_row_count); @@ -2851,8 +3549,7 @@ mod tests { assert_eq!( blocks_snapshot.is_line_replaced(buffer_row), expected_replaced_buffer_rows.contains(&buffer_row), - "incorrect is_line_replaced({:?})", - buffer_row + "incorrect is_line_replaced({buffer_row:?}), expected replaced rows: {expected_replaced_buffer_rows:?}", ); } } @@ -2869,7 +3566,7 @@ mod tests { fn as_custom(&self) -> Option<&CustomBlock> { match self { Block::Custom(block) => Some(block), - Block::ExcerptBoundary { .. } => None, + _ => None, } } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ab8a1893f5449..d3585c24fcf3c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -678,6 +678,7 @@ pub struct Editor { next_scroll_position: NextScrollCursorCenterTopBottom, addons: HashMap>, registered_buffers: HashMap, + 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) { - let selection = self.selections.newest::(cx); + if self.is_singleton(cx) { + let selection = self.selections.newest::(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) { - 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) { @@ -10432,22 +10489,30 @@ impl Editor { } pub fn fold_all(&mut self, _: &actions::FoldAll, cx: &mut ViewContext) { - 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) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let selections = self.selections.all::(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::>(); + 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::(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::>(); - 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) { @@ -10574,8 +10662,20 @@ impl Editor { } pub fn unfold_all(&mut self, _: &actions::UnfoldAll, cx: &mut ViewContext) { - 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) { @@ -10662,6 +10762,45 @@ impl Editor { }); } + pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext) { + 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) { + 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( &mut self, @@ -13820,6 +13959,10 @@ pub enum EditorEvent { ExcerptsRemoved { ids: Vec, }, + BufferFoldToggled { + ids: Vec, + folded: bool, + }, ExcerptsEdited { ids: Vec, }, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 921a4c0c28380..1436fed6bf962 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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::>(); + 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::>(); + 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::>(); + 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::( + 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 { let point = DisplayPoint::new(DisplayRow(row as u32), column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 6c049976bc408..c0431f6a06c62 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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) { + 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, 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, + ); + } } } } diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index 815825b606bf1..1e04fe4b73d43 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -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() } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index f804d4b1149b9..0e688e6cdd2bf 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -195,6 +195,7 @@ pub struct ExcerptInfo { pub buffer: BufferSnapshot, pub buffer_id: BufferId, pub range: ExcerptRange, + 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> { + 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 { 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); diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 16c229ddd411e..24ae0ba4a2f06 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -103,6 +103,7 @@ pub struct OutlinePanel { active_item: Option, _subscriptions: Vec, updating_fs_entries: bool, + new_entries_for_fs_update: HashSet, fs_entries_update_task: Task<()>, cached_entries_update_task: Task<()>, reveal_selection_task: Task>, @@ -116,6 +117,7 @@ pub struct OutlinePanel { horizontal_scrollbar_state: ScrollbarState, hide_scrollbar_task: Option>, max_width_item_index: Option, + preserve_selection_on_buffer_fold_toggles: HashSet, } #[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) { - 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) { + 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) { + 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) { + 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::>(); 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) { + 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, + fold: bool, + cx: &mut ViewContext, + ) -> 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) { @@ -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, - new_entries: HashSet, + active_editor: View, debounce: Option, cx: &mut ViewContext, ) { @@ -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, Option)> { 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, parent_entry: FsEntry, parent_depth: usize, filter_query: Option, is_singleton: bool, cx: &mut ViewContext, ) { - if self.active_editor().is_none() { - return; - }; let ItemsDisplayMode::Search(search_state) = &mut self.mode else { return; }; @@ -3560,10 +3772,27 @@ impl OutlinePanel { .collect::>(); 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 { + 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::>(); @@ -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(