diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 6d525a1ff0..479d63a76e 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -26,8 +26,8 @@ use collections::{BTreeSet, HashMap, HashSet}; use editor::{ actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt}, display_map::{ - BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease, - CreaseMetadata, CustomBlockId, FoldId, RenderBlock, ToDisplayPoint, + BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, + CustomBlockId, FoldId, RenderBlock, ToDisplayPoint, }, scroll::{Autoscroll, AutoscrollStrategy}, Anchor, Editor, EditorEvent, ProposedChangeLocation, ProposedChangesEditor, RowExt, @@ -2009,13 +2009,12 @@ impl ContextEditor { }) .map(|(command, error_message)| BlockProperties { style: BlockStyle::Fixed, - position: Anchor { + height: 1, + placement: BlockPlacement::Below(Anchor { buffer_id: Some(buffer_id), excerpt_id, text_anchor: command.source_range.start, - }, - height: 1, - disposition: BlockDisposition::Below, + }), render: slash_command_error_block_renderer(error_message), priority: 0, }), @@ -2242,11 +2241,10 @@ impl ContextEditor { } else { let block_ids = editor.insert_blocks( [BlockProperties { - position: patch_start, height: path_count as u32 + 1, style: BlockStyle::Flex, render: render_block, - disposition: BlockDisposition::Below, + placement: BlockPlacement::Below(patch_start), priority: 0, }], None, @@ -2731,12 +2729,13 @@ impl ContextEditor { }) }; let create_block_properties = |message: &Message| BlockProperties { - position: buffer - .anchor_in_excerpt(excerpt_id, message.anchor_range.start) - .unwrap(), height: 2, style: BlockStyle::Sticky, - disposition: BlockDisposition::Above, + placement: BlockPlacement::Above( + buffer + .anchor_in_excerpt(excerpt_id, message.anchor_range.start) + .unwrap(), + ), priority: usize::MAX, render: render_block(MessageMetadata::from(message)), }; @@ -3372,7 +3371,7 @@ impl ContextEditor { let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap(); let image = render_image.clone(); anchor.is_valid(&buffer).then(|| BlockProperties { - position: anchor, + placement: BlockPlacement::Above(anchor), height: MAX_HEIGHT_IN_LINES, style: BlockStyle::Sticky, render: Box::new(move |cx| { @@ -3393,8 +3392,6 @@ impl ContextEditor { ) .into_any_element() }), - - disposition: BlockDisposition::Above, priority: 0, }) }) diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 0b9ee0eae2..1134747d55 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -9,7 +9,7 @@ use collections::{hash_map, HashMap, HashSet, VecDeque}; use editor::{ actions::{MoveDown, MoveUp, SelectAll}, display_map::{ - BlockContext, BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, + BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, ToDisplayPoint, }, Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode, @@ -446,15 +446,14 @@ impl InlineAssistant { let assist_blocks = vec![ BlockProperties { style: BlockStyle::Sticky, - position: range.start, + placement: BlockPlacement::Above(range.start), height: prompt_editor_height, render: build_assist_editor_renderer(prompt_editor), - disposition: BlockDisposition::Above, priority: 0, }, BlockProperties { style: BlockStyle::Sticky, - position: range.end, + placement: BlockPlacement::Below(range.end), height: 0, render: Box::new(|cx| { v_flex() @@ -464,7 +463,6 @@ impl InlineAssistant { .border_color(cx.theme().status().info_border) .into_any_element() }), - disposition: BlockDisposition::Below, priority: 0, }, ]; @@ -1179,7 +1177,7 @@ impl InlineAssistant { let height = deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1); new_blocks.push(BlockProperties { - position: new_row, + placement: BlockPlacement::Above(new_row), height, style: BlockStyle::Flex, render: Box::new(move |cx| { @@ -1191,7 +1189,6 @@ impl InlineAssistant { .child(deleted_lines_editor.clone()) .into_any_element() }), - disposition: BlockDisposition::Above, priority: 0, }); } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 6876388542..cb6d07e906 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -9,7 +9,7 @@ use anyhow::Result; use collections::{BTreeSet, HashSet}; use editor::{ diagnostic_block_renderer, - display_map::{BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock}, + display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock}, highlight_diagnostic_message, scroll::Autoscroll, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, @@ -439,11 +439,10 @@ impl ProjectDiagnosticsEditor { primary.message.split('\n').next().unwrap().to_string(); group_state.block_count += 1; blocks_to_add.push(BlockProperties { - position: header_position, + placement: BlockPlacement::Above(header_position), height: 2, style: BlockStyle::Sticky, render: diagnostic_header_renderer(primary), - disposition: BlockDisposition::Above, priority: 0, }); } @@ -459,13 +458,15 @@ impl ProjectDiagnosticsEditor { if !diagnostic.message.is_empty() { group_state.block_count += 1; blocks_to_add.push(BlockProperties { - position: (excerpt_id, entry.range.start), + placement: BlockPlacement::Below(( + excerpt_id, + entry.range.start, + )), height: diagnostic.message.matches('\n').count() as u32 + 1, style: BlockStyle::Fixed, render: diagnostic_block_renderer( diagnostic, None, true, true, ), - disposition: BlockDisposition::Below, priority: 0, }); } @@ -498,13 +499,24 @@ impl ProjectDiagnosticsEditor { editor.remove_blocks(blocks_to_remove, None, cx); let block_ids = editor.insert_blocks( blocks_to_add.into_iter().flat_map(|block| { - let (excerpt_id, text_anchor) = block.position; + let placement = match block.placement { + BlockPlacement::Above((excerpt_id, text_anchor)) => BlockPlacement::Above( + excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?, + ), + BlockPlacement::Below((excerpt_id, text_anchor)) => BlockPlacement::Below( + excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?, + ), + BlockPlacement::Replace(_) => { + unreachable!( + "no Replace block should have been pushed to blocks_to_add" + ) + } + }; Some(BlockProperties { - position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?, + placement, height: block.height, style: block.style, render: block.render, - disposition: block.disposition, priority: 0, }) }), diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 528385ebab..e24336d1e9 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -29,8 +29,8 @@ use crate::{ hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt, }; pub use block_map::{ - Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId, - BlockMap, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, + Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, + BlockPlacement, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, }; use block_map::{BlockRow, BlockSnapshot}; use char_map::{CharMap, CharSnapshot}; @@ -1180,6 +1180,7 @@ impl ToDisplayPoint for Anchor { pub mod tests { use super::*; use crate::{movement, test::marked_display_snapshot}; + use block_map::BlockPlacement; use gpui::{div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla}; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, @@ -1293,24 +1294,22 @@ pub mod tests { Bias::Left, )); - let disposition = if rng.gen() { - BlockDisposition::Above + let placement = if rng.gen() { + BlockPlacement::Above(position) } else { - BlockDisposition::Below + BlockPlacement::Below(position) }; let height = rng.gen_range(1..5); log::info!( - "inserting block {:?} {:?} with height {}", - disposition, - position.to_point(&buffer), + "inserting block {:?} with height {}", + placement.as_ref().map(|p| p.to_point(&buffer)), height ); let priority = rng.gen_range(1..100); BlockProperties { + placement, style: BlockStyle::Fixed, - position, height, - disposition, render: Box::new(|_| div().into_any()), priority, } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 6b0d45fc76..44a540bc95 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -6,7 +6,9 @@ use crate::{EditorStyle, GutterDimensions}; use collections::{Bound, HashMap, HashSet}; use gpui::{AnyElement, EntityId, Pixels, WindowContext}; use language::{Chunk, Patch, Point}; -use multi_buffer::{Anchor, ExcerptId, ExcerptInfo, MultiBufferRow, ToPoint as _}; +use multi_buffer::{ + Anchor, ExcerptId, ExcerptInfo, MultiBufferRow, MultiBufferSnapshot, ToPoint as _, +}; use parking_lot::Mutex; use std::{ cell::RefCell, @@ -18,7 +20,7 @@ use std::{ Arc, }, }; -use sum_tree::{Bias, SumTree, TreeMap}; +use sum_tree::{Bias, SumTree, Summary, TreeMap}; use text::Edit; use ui::ElementId; @@ -77,32 +79,173 @@ struct WrapRow(u32); pub type RenderBlock = Box AnyElement>; +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum BlockPlacement { + Above(T), + Below(T), + Replace(Range), +} + +impl BlockPlacement { + fn start(&self) -> &T { + match self { + BlockPlacement::Above(position) => position, + BlockPlacement::Below(position) => position, + BlockPlacement::Replace(range) => &range.start, + } + } + + fn end(&self) -> &T { + match self { + BlockPlacement::Above(position) => position, + BlockPlacement::Below(position) => position, + BlockPlacement::Replace(range) => &range.end, + } + } + + pub fn as_ref(&self) -> BlockPlacement<&T> { + match self { + BlockPlacement::Above(position) => BlockPlacement::Above(position), + BlockPlacement::Below(position) => BlockPlacement::Below(position), + BlockPlacement::Replace(range) => BlockPlacement::Replace(&range.start..&range.end), + } + } + + pub fn map(self, mut f: impl FnMut(T) -> R) -> 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)), + } + } +} + +impl BlockPlacement { + fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering { + match (self, other) { + (BlockPlacement::Above(anchor_a), BlockPlacement::Above(anchor_b)) + | (BlockPlacement::Below(anchor_a), BlockPlacement::Below(anchor_b)) => { + anchor_a.cmp(anchor_b, buffer) + } + (BlockPlacement::Above(anchor_a), BlockPlacement::Below(anchor_b)) => { + anchor_a.cmp(anchor_b, buffer).then(Ordering::Less) + } + (BlockPlacement::Below(anchor_a), BlockPlacement::Above(anchor_b)) => { + anchor_a.cmp(anchor_b, buffer).then(Ordering::Greater) + } + (BlockPlacement::Above(anchor), BlockPlacement::Replace(range)) => { + anchor.cmp(&range.start, buffer).then(Ordering::Less) + } + (BlockPlacement::Replace(range), BlockPlacement::Above(anchor)) => { + range.start.cmp(anchor, buffer).then(Ordering::Greater) + } + (BlockPlacement::Below(anchor), BlockPlacement::Replace(range)) => { + anchor.cmp(&range.start, buffer).then(Ordering::Greater) + } + (BlockPlacement::Replace(range), BlockPlacement::Below(anchor)) => { + 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)), + } + } + + fn to_wrap_row(&self, wrap_snapshot: &WrapSnapshot) -> Option> { + let buffer_snapshot = wrap_snapshot.buffer_snapshot(); + match self { + BlockPlacement::Above(position) => { + let mut position = position.to_point(buffer_snapshot); + position.column = 0; + let wrap_row = WrapRow(wrap_snapshot.make_wrap_point(position, Bias::Left).row()); + Some(BlockPlacement::Above(wrap_row)) + } + BlockPlacement::Below(position) => { + let mut position = position.to_point(buffer_snapshot); + position.column = buffer_snapshot.line_len(MultiBufferRow(position.row)); + let wrap_row = WrapRow(wrap_snapshot.make_wrap_point(position, Bias::Left).row()); + 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); + if start == end { + None + } else { + start.column = 0; + let start_wrap_row = + WrapRow(wrap_snapshot.make_wrap_point(start, Bias::Left).row()); + 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)) + } + } + } + } +} + +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, - position: Anchor, + placement: BlockPlacement, height: u32, style: BlockStyle, render: Arc>, - disposition: BlockDisposition, priority: usize, } pub struct BlockProperties

{ - pub position: P, + pub placement: BlockPlacement

, pub height: u32, pub style: BlockStyle, pub render: RenderBlock, - pub disposition: BlockDisposition, pub priority: usize, } impl Debug for BlockProperties

{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BlockProperties") - .field("position", &self.position) + .field("placement", &self.placement) .field("height", &self.height) .field("style", &self.style) - .field("disposition", &self.disposition) .finish() } } @@ -125,10 +268,10 @@ pub struct BlockContext<'a, 'b> { pub editor_style: &'b EditorStyle, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)] pub enum BlockId { - Custom(CustomBlockId), ExcerptBoundary(Option), + Custom(CustomBlockId), } impl From for ElementId { @@ -152,30 +295,12 @@ impl std::fmt::Display for BlockId { } } -/// Whether the block should be considered above or below the anchor line -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum BlockDisposition { - Above, - Below, -} - #[derive(Clone, Debug)] struct Transform { summary: TransformSummary, block: Option, } -pub(crate) enum BlockType { - Custom(CustomBlockId), - ExcerptBoundary, -} - -pub(crate) trait BlockLike { - fn block_type(&self) -> BlockType; - fn disposition(&self) -> BlockDisposition; - fn priority(&self) -> usize; -} - #[allow(clippy::large_enum_variant)] #[derive(Clone)] pub enum Block { @@ -189,26 +314,6 @@ pub enum Block { }, } -impl BlockLike for Block { - fn block_type(&self) -> BlockType { - match self { - Block::Custom(block) => BlockType::Custom(block.id), - Block::ExcerptBoundary { .. } => BlockType::ExcerptBoundary, - } - } - - fn disposition(&self) -> BlockDisposition { - self.disposition() - } - - fn priority(&self) -> usize { - match self { - Block::Custom(block) => block.priority, - Block::ExcerptBoundary { .. } => usize::MAX, - } - } -} - impl Block { pub fn id(&self) -> BlockId { match self { @@ -219,19 +324,6 @@ impl Block { } } - fn disposition(&self) -> BlockDisposition { - match self { - Block::Custom(block) => block.disposition, - Block::ExcerptBoundary { next_excerpt, .. } => { - if next_excerpt.is_some() { - BlockDisposition::Above - } else { - BlockDisposition::Below - } - } - } - } - pub fn height(&self) -> u32 { match self { Block::Custom(block) => block.height, @@ -245,6 +337,20 @@ impl Block { Block::ExcerptBoundary { .. } => BlockStyle::Sticky, } } + + fn place_above(&self) -> bool { + match self { + Block::Custom(block) => matches!(block.placement, BlockPlacement::Above(_)), + Block::ExcerptBoundary { next_excerpt, .. } => next_excerpt.is_some(), + } + } + + fn place_below(&self) -> bool { + match self { + Block::Custom(block) => matches!(block.placement, BlockPlacement::Below(_)), + Block::ExcerptBoundary { next_excerpt, .. } => next_excerpt.is_none(), + } + } } impl Debug for Block { @@ -270,6 +376,8 @@ impl Debug for Block { struct TransformSummary { input_rows: u32, output_rows: u32, + longest_row: u32, + longest_row_chars: u32, } pub struct BlockChunks<'a> { @@ -298,11 +406,13 @@ impl BlockMap { excerpt_footer_height: u32, ) -> Self { let row_count = wrap_snapshot.max_point().row() + 1; + let mut transforms = SumTree::default(); + push_isomorphic(&mut transforms, row_count, &wrap_snapshot); let map = Self { next_block_id: AtomicUsize::new(0), custom_blocks: Vec::new(), custom_blocks_by_id: TreeMap::default(), - transforms: RefCell::new(SumTree::from_item(Transform::isomorphic(row_count), &())), + transforms: RefCell::new(transforms), wrap_snapshot: RefCell::new(wrap_snapshot.clone()), show_excerpt_controls, buffer_header_height, @@ -368,28 +478,29 @@ impl BlockMap { let mut transforms = self.transforms.borrow_mut(); let mut new_transforms = SumTree::default(); - let old_row_count = transforms.summary().input_rows; - let new_row_count = wrap_snapshot.max_point().row() + 1; let mut cursor = transforms.cursor::(&()); let mut last_block_ix = 0; let mut blocks_in_edit = Vec::new(); let mut edits = edits.into_iter().peekable(); while let Some(edit) = edits.next() { - // Preserve any old transforms that precede this edit. - let old_start = WrapRow(edit.old.start); - let new_start = WrapRow(edit.new.start); + 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 new_transforms.append(cursor.slice(&old_start, Bias::Left, &()), &()); if let Some(transform) = cursor.item() { - if transform.is_isomorphic() && old_start == cursor.end(&()) { + if transform.summary.input_rows > 0 && cursor.end(&()) == old_start { + // Preserve the transform (push and next) new_transforms.push(transform.clone(), &()); cursor.next(&()); + + // Preserve below blocks at end of edit while let Some(transform) = cursor.item() { - if transform - .block - .as_ref() - .map_or(false, |b| b.disposition().is_below()) - { + if transform.block.as_ref().map_or(false, |b| b.place_below()) { new_transforms.push(transform.clone(), &()); cursor.next(&()); } else { @@ -399,50 +510,70 @@ impl BlockMap { } } - // Preserve any portion of an old transform that precedes this edit. - let extent_before_edit = old_start.0 - cursor.start().0; - push_isomorphic(&mut new_transforms, extent_before_edit); + // 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 { + if transform.block.is_none() { + // Preserve any portion of the old isomorphic transform that precedes this edit. + push_isomorphic( + &mut new_transforms, + transform_rows_before_edit, + wrap_snapshot, + ); + } else { + // We landed within a block that replaces some lines, so we + // extend the edit to start at the beginning of the + // replacement. + 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; + } + } - // Skip over any old transforms that intersect this edit. + // Decide where the edit ends + // * It should end at a transform boundary + // * Coalesce edits that intersect the same transform let mut old_end = WrapRow(edit.old.end); let mut new_end = WrapRow(edit.new.end); - cursor.seek(&old_end, Bias::Left, &()); - cursor.next(&()); - if old_end == *cursor.start() { - while let Some(transform) = cursor.item() { - if transform - .block - .as_ref() - .map_or(false, |b| b.disposition().is_below()) - { + loop { + // Seek to the transform starting at or after the end of the edit + cursor.seek(&old_end, Bias::Left, &()); + cursor.next(&()); + + // Extend edit to the end of the discarded transform so it is reconstructed in full + let transform_rows_after_edit = cursor.start().0 - old_end.0; + old_end.0 += transform_rows_after_edit; + new_end.0 += transform_rows_after_edit; + + // Combine this edit with any subsequent edits that intersect the same transform. + while let Some(next_edit) = edits.peek() { + if next_edit.old.start <= cursor.start().0 { + old_end = WrapRow(next_edit.old.end); + new_end = WrapRow(next_edit.new.end); + cursor.seek(&old_end, Bias::Left, &()); cursor.next(&()); + edits.next(); } else { break; } } + + if *cursor.start() == old_end { + break; + } } - // Combine this edit with any subsequent edits that intersect the same transform. - while let Some(next_edit) = edits.peek() { - if next_edit.old.start <= cursor.start().0 { - old_end = WrapRow(next_edit.old.end); - new_end = WrapRow(next_edit.new.end); - cursor.seek(&old_end, Bias::Left, &()); + // Discard below blocks at the end of the edit. They'll be reconstructed. + while let Some(transform) = cursor.item() { + if transform.block.as_ref().map_or(false, |b| b.place_below()) { cursor.next(&()); - if old_end == *cursor.start() { - while let Some(transform) = cursor.item() { - if transform - .block - .as_ref() - .map_or(false, |b| b.disposition().is_below()) - { - cursor.next(&()); - } else { - break; - } - } - } - edits.next(); } else { break; } @@ -455,9 +586,10 @@ impl BlockMap { let start_block_ix = match self.custom_blocks[last_block_ix..].binary_search_by(|probe| { probe - .position + .start() .to_point(buffer) .cmp(&new_buffer_start) + // Move left until we find the index of the first block starting within this edit .then(Ordering::Greater) }) { Ok(ix) | Err(ix) => last_block_ix + ix, @@ -473,7 +605,7 @@ impl BlockMap { end_bound = Bound::Excluded(new_buffer_end); match self.custom_blocks[start_block_ix..].binary_search_by(|probe| { probe - .position + .start() .to_point(buffer) .cmp(&new_buffer_end) .then(Ordering::Greater) @@ -484,19 +616,17 @@ impl BlockMap { last_block_ix = end_block_ix; debug_assert!(blocks_in_edit.is_empty()); - blocks_in_edit.extend(self.custom_blocks[start_block_ix..end_block_ix].iter().map( - |block| { - let mut position = block.position.to_point(buffer); - match block.disposition { - BlockDisposition::Above => position.column = 0, - BlockDisposition::Below => { - position.column = buffer.line_len(MultiBufferRow(position.row)) - } - } - let position = wrap_snapshot.make_wrap_point(position, Bias::Left); - (position.row(), Block::Custom(block.clone())) - }, - )); + + blocks_in_edit.extend( + self.custom_blocks[start_block_ix..end_block_ix] + .iter() + .filter_map(|block| { + Some(( + block.placement.to_wrap_row(wrap_snapshot)?, + Block::Custom(block.clone()), + )) + }), + ); if buffer.show_headers() { blocks_in_edit.extend(BlockMap::header_and_footer_blocks( @@ -514,26 +644,49 @@ impl BlockMap { // For each of these blocks, insert a new isomorphic transform preceding the block, // and then insert the block itself. - for (block_row, block) in blocks_in_edit.drain(..) { - let insertion_row = match block.disposition() { - BlockDisposition::Above => block_row, - BlockDisposition::Below => block_row + 1, + 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(), + longest_row: 0, + longest_row_chars: 0, }; - let extent_before_block = insertion_row - new_transforms.summary().input_rows; - push_isomorphic(&mut new_transforms, extent_before_block); - new_transforms.push(Transform::block(block), &()); + + let rows_before_block; + match block_placement { + BlockPlacement::Above(position) => { + rows_before_block = position.0 - new_transforms.summary().input_rows; + } + BlockPlacement::Below(position) => { + 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; + } + } + + push_isomorphic(&mut new_transforms, rows_before_block, wrap_snapshot); + new_transforms.push( + Transform { + summary, + block: Some(block), + }, + &(), + ); } - old_end = WrapRow(old_end.0.min(old_row_count)); - new_end = WrapRow(new_end.0.min(new_row_count)); - // Insert an isomorphic transform after the final block. - let extent_after_last_block = new_end.0 - new_transforms.summary().input_rows; - push_isomorphic(&mut new_transforms, extent_after_last_block); - - // Preserve any portion of the old transform after this edit. - let extent_after_edit = cursor.start().0 - old_end.0; - push_isomorphic(&mut new_transforms, extent_after_edit); + let rows_after_last_block = new_end + .0 + .saturating_sub(new_transforms.summary().input_rows); + push_isomorphic(&mut new_transforms, rows_after_last_block, wrap_snapshot); } new_transforms.append(cursor.suffix(&()), &()); @@ -558,7 +711,7 @@ impl BlockMap { self.show_excerpt_controls } - pub fn header_and_footer_blocks<'a, 'b: 'a, 'c: 'a + 'b, R, T>( + fn header_and_footer_blocks<'a, 'b: 'a, 'c: 'a + 'b, R, T>( show_excerpt_controls: bool, excerpt_footer_height: u32, buffer_header_height: u32, @@ -566,7 +719,7 @@ impl BlockMap { buffer: &'b multi_buffer::MultiBufferSnapshot, range: R, wrap_snapshot: &'c WrapSnapshot, - ) -> impl Iterator + 'b + ) -> impl Iterator, Block)> + 'b where R: RangeBounds, T: multi_buffer::ToOffset, @@ -619,7 +772,11 @@ impl BlockMap { } Some(( - wrap_row, + 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, @@ -631,45 +788,96 @@ impl BlockMap { }) } - pub(crate) fn sort_blocks(blocks: &mut [(u32, B)]) { - // Place excerpt headers and footers above custom blocks on the same row - blocks.sort_unstable_by(|(row_a, block_a), (row_b, block_b)| { - row_a.cmp(row_b).then_with(|| { - block_a - .disposition() - .cmp(&block_b.disposition()) - .then_with(|| match ((block_a.block_type()), (block_b.block_type())) { - (BlockType::ExcerptBoundary, BlockType::ExcerptBoundary) => Ordering::Equal, - (BlockType::ExcerptBoundary, _) => Ordering::Less, - (_, BlockType::ExcerptBoundary) => Ordering::Greater, - (BlockType::Custom(a_id), BlockType::Custom(b_id)) => block_b - .priority() - .cmp(&block_a.priority()) - .then_with(|| a_id.cmp(&b_id)), - }) - }) + 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() { + Ordering::Less + } else { + Ordering::Greater + } + } + (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)), + }) + }); + 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 + } + (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); + true + } else { + false + } + } + _ => false, }); } } -fn push_isomorphic(tree: &mut SumTree, rows: u32) { +fn push_isomorphic(tree: &mut SumTree, rows: u32, wrap_snapshot: &WrapSnapshot) { if rows == 0 { return; } - let mut extent = Some(rows); + let wrap_row_start = tree.summary().input_rows; + let wrap_row_end = wrap_row_start + rows; + let wrap_summary = wrap_snapshot.text_summary_for_range(wrap_row_start..wrap_row_end); + let summary = TransformSummary { + input_rows: rows, + output_rows: rows, + longest_row: wrap_summary.longest_row, + longest_row_chars: wrap_summary.longest_row_chars, + }; + let mut merged = false; tree.update_last( |last_transform| { - if last_transform.is_isomorphic() { - let extent = extent.take().unwrap(); - last_transform.summary.input_rows += extent; - last_transform.summary.output_rows += extent; + if last_transform.block.is_none() { + last_transform.summary.add_summary(&summary, &()); + merged = true; } }, &(), ); - if let Some(extent) = extent { - tree.push(Transform::isomorphic(extent), &()); + if !merged { + tree.push( + Transform { + summary, + block: None, + }, + &(), + ); } } @@ -711,7 +919,7 @@ impl<'a> BlockMapReader<'a> { pub fn row_for_block(&self, block_id: CustomBlockId) -> Option { let block = self.blocks.iter().find(|block| block.id == block_id)?; let buffer_row = block - .position + .start() .to_point(self.wrap_snapshot.buffer_snapshot()) .row; let wrap_row = self @@ -735,9 +943,7 @@ impl<'a> BlockMapReader<'a> { break; } - if let Some(BlockType::Custom(id)) = - transform.block.as_ref().map(|block| block.block_type()) - { + if let Some(BlockId::Custom(id)) = transform.block.as_ref().map(|block| block.id()) { if id == block_id { return Some(cursor.start().1); } @@ -762,21 +968,27 @@ impl<'a> BlockMapWriter<'a> { let mut previous_wrap_row_range: Option> = None; for block in blocks { + if let BlockPlacement::Replace(_) = &block.placement { + debug_assert!(block.height > 0); + } + let id = CustomBlockId(self.0.next_block_id.fetch_add(1, SeqCst)); ids.push(id); - let position = block.position; - let point = position.to_point(buffer); - let wrap_row = wrap_snapshot - .make_wrap_point(Point::new(point.row, 0), Bias::Left) - .row(); + let start = block.placement.start().to_point(buffer); + let end = block.placement.end().to_point(buffer); + let start_wrap_row = wrap_snapshot.make_wrap_point(start, Bias::Left).row(); + let end_wrap_row = wrap_snapshot.make_wrap_point(end, Bias::Left).row(); let (start_row, end_row) = { - previous_wrap_row_range.take_if(|range| !range.contains(&wrap_row)); + previous_wrap_row_range.take_if(|range| { + !range.contains(&start_wrap_row) || !range.contains(&end_wrap_row) + }); let range = previous_wrap_row_range.get_or_insert_with(|| { - let start_row = wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0)); + let start_row = + wrap_snapshot.prev_row_boundary(WrapPoint::new(start_wrap_row, 0)); let end_row = wrap_snapshot - .next_row_boundary(WrapPoint::new(wrap_row, 0)) + .next_row_boundary(WrapPoint::new(end_wrap_row, 0)) .unwrap_or(wrap_snapshot.max_point().row() + 1); start_row..end_row }); @@ -785,16 +997,15 @@ impl<'a> BlockMapWriter<'a> { let block_ix = match self .0 .custom_blocks - .binary_search_by(|probe| probe.position.cmp(&position, buffer)) + .binary_search_by(|probe| probe.placement.cmp(&block.placement, buffer)) { Ok(ix) | Err(ix) => ix, }; let new_block = Arc::new(CustomBlock { id, - position, + placement: block.placement, height: block.height, render: Arc::new(Mutex::new(block.render)), - disposition: block.disposition, style: block.style, priority: block.priority, }); @@ -819,34 +1030,41 @@ impl<'a> BlockMapWriter<'a> { for block in &mut self.0.custom_blocks { if let Some(new_height) = heights.remove(&block.id) { + if let BlockPlacement::Replace(_) = &block.placement { + debug_assert!(new_height > 0); + } + if block.height != new_height { let new_block = CustomBlock { id: block.id, - position: block.position, + placement: block.placement.clone(), height: new_height, style: block.style, render: block.render.clone(), - disposition: block.disposition, priority: block.priority, }; let new_block = Arc::new(new_block); *block = new_block.clone(); self.0.custom_blocks_by_id.insert(block.id, new_block); - let buffer_row = block.position.to_point(buffer).row; - if last_block_buffer_row != Some(buffer_row) { - last_block_buffer_row = Some(buffer_row); - let wrap_row = wrap_snapshot - .make_wrap_point(Point::new(buffer_row, 0), Bias::Left) + let start_row = block.placement.start().to_point(buffer).row; + let end_row = block.placement.end().to_point(buffer).row; + if last_block_buffer_row != Some(end_row) { + last_block_buffer_row = Some(end_row); + let start_wrap_row = wrap_snapshot + .make_wrap_point(Point::new(start_row, 0), Bias::Left) .row(); - let start_row = - wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0)); - let end_row = wrap_snapshot - .next_row_boundary(WrapPoint::new(wrap_row, 0)) + let end_wrap_row = wrap_snapshot + .make_wrap_point(Point::new(end_row, 0), Bias::Left) + .row(); + let start = + wrap_snapshot.prev_row_boundary(WrapPoint::new(start_wrap_row, 0)); + let end = wrap_snapshot + .next_row_boundary(WrapPoint::new(end_wrap_row, 0)) .unwrap_or(wrap_snapshot.max_point().row() + 1); edits.push(Edit { - old: start_row..end_row, - new: start_row..end_row, + old: start..end, + new: start..end, }) } } @@ -864,19 +1082,21 @@ impl<'a> BlockMapWriter<'a> { let mut previous_wrap_row_range: Option> = None; self.0.custom_blocks.retain(|block| { if block_ids.contains(&block.id) { - let buffer_row = block.position.to_point(buffer).row; - if last_block_buffer_row != Some(buffer_row) { - last_block_buffer_row = Some(buffer_row); - let wrap_row = wrap_snapshot - .make_wrap_point(Point::new(buffer_row, 0), Bias::Left) - .row(); + let start = block.placement.start().to_point(buffer); + let end = block.placement.end().to_point(buffer); + if last_block_buffer_row != Some(end.row) { + last_block_buffer_row = Some(end.row); + let start_wrap_row = wrap_snapshot.make_wrap_point(start, Bias::Left).row(); + let end_wrap_row = wrap_snapshot.make_wrap_point(end, Bias::Left).row(); let (start_row, end_row) = { - previous_wrap_row_range.take_if(|range| !range.contains(&wrap_row)); + previous_wrap_row_range.take_if(|range| { + !range.contains(&start_wrap_row) || !range.contains(&end_wrap_row) + }); let range = previous_wrap_row_range.get_or_insert_with(|| { let start_row = - wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0)); + wrap_snapshot.prev_row_boundary(WrapPoint::new(start_wrap_row, 0)); let end_row = wrap_snapshot - .next_row_boundary(WrapPoint::new(wrap_row, 0)) + .next_row_boundary(WrapPoint::new(end_wrap_row, 0)) .unwrap_or(wrap_snapshot.max_point().row() + 1); start_row..end_row }); @@ -921,31 +1141,24 @@ impl BlockSnapshot { highlights: Highlights<'a>, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - let input_end = { - cursor.seek(&BlockRow(rows.end), Bias::Right, &()); - let overshoot = if cursor - .item() - .map_or(false, |transform| transform.is_isomorphic()) - { - rows.end - cursor.start().0 .0 - } else { - 0 - }; - cursor.start().1 .0 + overshoot - }; - let input_start = { - cursor.seek(&BlockRow(rows.start), Bias::Right, &()); - let overshoot = if cursor - .item() - .map_or(false, |transform| transform.is_isomorphic()) - { - rows.start - cursor.start().0 .0 - } else { - 0 - }; - cursor.start().1 .0 + overshoot - }; + cursor.seek(&BlockRow(rows.start), Bias::Right, &()); + let transform_output_start = cursor.start().0 .0; + let transform_input_start = cursor.start().1 .0; + + let mut input_start = transform_input_start; + let mut input_end = transform_input_start; + if let Some(transform) = cursor.item() { + if transform.block.is_none() { + input_start += rows.start - transform_output_start; + input_end += cmp::min( + rows.end - transform_output_start, + transform.summary.input_rows, + ); + } + } + BlockChunks { input_chunks: self.wrap_snapshot.chunks( input_start..input_end, @@ -964,7 +1177,10 @@ impl BlockSnapshot { let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&start_row, Bias::Right, &()); let (output_start, input_start) = cursor.start(); - let overshoot = if cursor.item().map_or(false, |t| t.is_isomorphic()) { + let overshoot = if cursor + .item() + .map_or(false, |transform| transform.block.is_none()) + { start_row.0 - output_start.0 } else { 0 @@ -1049,13 +1265,12 @@ impl BlockSnapshot { } pub fn max_point(&self) -> BlockPoint { - let row = self.transforms.summary().output_rows - 1; + let row = self.transforms.summary().output_rows.saturating_sub(1); BlockPoint::new(row, self.line_len(BlockRow(row))) } pub fn longest_row(&self) -> u32 { - let input_row = self.wrap_snapshot.longest_row(); - self.to_block_point(WrapPoint::new(input_row, 0)).row + self.transforms.summary().longest_row } pub(super) fn line_len(&self, row: BlockRow) -> u32 { @@ -1069,6 +1284,8 @@ impl BlockSnapshot { } else { self.wrap_snapshot.line_len(input_start.0 + overshoot) } + } else if row.0 == 0 { + 0 } else { panic!("row out of range"); } @@ -1091,26 +1308,40 @@ impl BlockSnapshot { loop { if let Some(transform) = cursor.item() { - if transform.is_isomorphic() { - let (output_start_row, input_start_row) = cursor.start(); - let (output_end_row, input_end_row) = cursor.end(&()); - let output_start = Point::new(output_start_row.0, 0); - let input_start = Point::new(input_start_row.0, 0); - let input_end = Point::new(input_end_row.0, 0); - let input_point = if point.row >= output_end_row.0 { - let line_len = self.wrap_snapshot.line_len(input_end_row.0 - 1); - self.wrap_snapshot - .clip_point(WrapPoint::new(input_end_row.0 - 1, line_len), bias) - } else { - let output_overshoot = point.0.saturating_sub(output_start); - self.wrap_snapshot - .clip_point(WrapPoint(input_start + output_overshoot), bias) - }; + let (output_start_row, input_start_row) = cursor.start(); + let (output_end_row, input_end_row) = cursor.end(&()); + let output_start = Point::new(output_start_row.0, 0); + let output_end = Point::new(output_end_row.0, 0); + let input_start = Point::new(input_start_row.0, 0); + let input_end = Point::new(input_end_row.0, 0); - if (input_start..input_end).contains(&input_point.0) { - let input_overshoot = input_point.0.saturating_sub(input_start); - return BlockPoint(output_start + input_overshoot); + match transform.block.as_ref() { + Some(Block::Custom(block)) + if matches!(block.placement, BlockPlacement::Replace(_)) => + { + if bias == Bias::Left { + return BlockPoint(output_start); + } else { + return BlockPoint(Point::new(output_end.row - 1, 0)); + } } + None => { + let input_point = if point.row >= output_end_row.0 { + let line_len = self.wrap_snapshot.line_len(input_end_row.0 - 1); + self.wrap_snapshot + .clip_point(WrapPoint::new(input_end_row.0 - 1, line_len), bias) + } else { + let output_overshoot = point.0.saturating_sub(output_start); + self.wrap_snapshot + .clip_point(WrapPoint(input_start + output_overshoot), bias) + }; + + if (input_start..input_end).contains(&input_point.0) { + let input_overshoot = input_point.0.saturating_sub(input_start); + return BlockPoint(output_start + input_overshoot); + } + } + _ => {} } if search_left { @@ -1132,27 +1363,40 @@ impl BlockSnapshot { let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &()); if let Some(transform) = cursor.item() { - debug_assert!(transform.is_isomorphic()); + if transform.block.is_some() { + let wrap_start = WrapPoint::new(cursor.start().0 .0, 0); + if wrap_start == wrap_point { + BlockPoint::new(cursor.start().1 .0, 0) + } else { + BlockPoint::new(cursor.end(&()).1 .0 - 1, 0) + } + } else { + let (input_start_row, output_start_row) = cursor.start(); + let input_start = Point::new(input_start_row.0, 0); + let output_start = Point::new(output_start_row.0, 0); + let input_overshoot = wrap_point.0 - input_start; + BlockPoint(output_start + input_overshoot) + } } else { - return self.max_point(); + self.max_point() } - - let (input_start_row, output_start_row) = cursor.start(); - let input_start = Point::new(input_start_row.0, 0); - let output_start = Point::new(output_start_row.0, 0); - let input_overshoot = wrap_point.0 - input_start; - BlockPoint(output_start + input_overshoot) } pub fn to_wrap_point(&self, block_point: BlockPoint) -> WrapPoint { let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&BlockRow(block_point.row), Bias::Right, &()); if let Some(transform) = cursor.item() { - match transform.block.as_ref().map(|b| b.disposition()) { - Some(BlockDisposition::Above) => WrapPoint::new(cursor.start().1 .0, 0), - Some(BlockDisposition::Below) => { - let wrap_row = cursor.start().1 .0 - 1; - WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row)) + match transform.block.as_ref() { + Some(block) => { + if block.place_below() { + let wrap_row = cursor.start().1 .0 - 1; + WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row)) + } else if block.place_above() || block_point.row == cursor.start().0 .0 { + WrapPoint::new(cursor.start().1 .0, 0) + } else { + let wrap_row = cursor.end(&()).1 .0 - 1; + WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row)) + } } None => { let overshoot = block_point.row - cursor.start().0 .0; @@ -1166,33 +1410,8 @@ impl BlockSnapshot { } } -impl Transform { - fn isomorphic(rows: u32) -> Self { - Self { - summary: TransformSummary { - input_rows: rows, - output_rows: rows, - }, - block: None, - } - } - - fn block(block: Block) -> Self { - Self { - summary: TransformSummary { - input_rows: 0, - output_rows: block.height(), - }, - block: Some(block), - } - } - - fn is_isomorphic(&self) -> bool { - self.block.is_none() - } -} - impl<'a> BlockChunks<'a> { + /// Go to the next transform fn advance(&mut self) { self.transforms.next(&()); while let Some(transform) = self.transforms.item() { @@ -1206,6 +1425,23 @@ impl<'a> BlockChunks<'a> { break; } } + + if self + .transforms + .item() + .map_or(false, |transform| transform.block.is_none()) + { + let start_input_row = self.transforms.start().1 .0; + let start_output_row = self.transforms.start().0 .0; + if start_output_row < self.max_output_row { + let end_input_row = cmp::min( + self.transforms.end(&()).1 .0, + start_input_row + (self.max_output_row - start_output_row), + ); + self.input_chunks.seek(start_input_row..end_input_row); + } + self.input_chunk = Chunk::default(); + } } } @@ -1241,16 +1477,17 @@ impl<'a> Iterator for BlockChunks<'a> { if let Some(input_chunk) = self.input_chunks.next() { self.input_chunk = input_chunk; } else { - self.output_row += 1; if self.output_row < self.max_output_row { + self.output_row += 1; self.advance(); - return Some(Chunk { - text: "\n", - ..Default::default() - }); - } else { - return None; + if self.transforms.item().is_some() { + return Some(Chunk { + text: "\n", + ..Default::default() + }); + } } + return None; } } @@ -1258,6 +1495,7 @@ impl<'a> Iterator for BlockChunks<'a> { let (prefix_rows, prefix_bytes) = offset_for_row(self.input_chunk.text, transform_end - self.output_row); self.output_row += prefix_rows; + let (mut prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes); self.input_chunk.text = suffix; if self.output_row == transform_end { @@ -1291,17 +1529,20 @@ impl<'a> Iterator for BlockBufferRows<'a> { if self.output_row.0 >= self.transforms.end(&()).0 .0 { self.transforms.next(&()); - } + while let Some(transform) = self.transforms.item() { + if transform + .block + .as_ref() + .map_or(false, |block| block.height() == 0) + { + self.transforms.next(&()); + } else { + break; + } + } - while let Some(transform) = self.transforms.item() { - if transform - .block - .as_ref() - .map_or(false, |block| block.height() == 0) - { - self.transforms.next(&()); - } else { - break; + if self.transforms.item()?.block.is_none() { + self.input_buffer_rows.seek(self.transforms.start().1 .0); } } @@ -1330,6 +1571,10 @@ impl sum_tree::Summary for TransformSummary { } fn add_summary(&mut self, summary: &Self, _: &()) { + if summary.longest_row_chars > self.longest_row_chars { + self.longest_row = self.output_rows + summary.longest_row; + self.longest_row_chars = summary.longest_row_chars; + } self.input_rows += summary.input_rows; self.output_rows += summary.output_rows; } @@ -1355,12 +1600,6 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for BlockRow { } } -impl BlockDisposition { - fn is_below(&self) -> bool { - matches!(self, BlockDisposition::Below) - } -} - impl<'a> Deref for BlockContext<'a, '_> { type Target = WindowContext<'a>; @@ -1380,8 +1619,12 @@ impl CustomBlock { self.render.lock()(cx) } - pub fn position(&self) -> &Anchor { - &self.position + pub fn start(&self) -> Anchor { + *self.placement.start() + } + + pub fn end(&self) -> Anchor { + *self.placement.end() } pub fn style(&self) -> BlockStyle { @@ -1393,9 +1636,11 @@ impl Debug for CustomBlock { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Block") .field("id", &self.id) - .field("position", &self.position) - .field("disposition", &self.disposition) - .finish() + .field("placement", &self.placement) + .field("height", &self.height) + .field("style", &self.style) + .field("priority", &self.priority) + .finish_non_exhaustive() } } @@ -1465,25 +1710,22 @@ mod tests { let block_ids = writer.insert(vec![ BlockProperties { style: BlockStyle::Fixed, - position: buffer_snapshot.anchor_after(Point::new(1, 0)), + placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 0))), height: 1, - disposition: BlockDisposition::Above, render: Box::new(|_| div().into_any()), priority: 0, }, BlockProperties { style: BlockStyle::Fixed, - position: buffer_snapshot.anchor_after(Point::new(1, 2)), + placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 2))), height: 2, - disposition: BlockDisposition::Above, render: Box::new(|_| div().into_any()), priority: 0, }, BlockProperties { style: BlockStyle::Fixed, - position: buffer_snapshot.anchor_after(Point::new(3, 3)), + placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(3, 3))), height: 3, - disposition: BlockDisposition::Below, render: Box::new(|_| div().into_any()), priority: 0, }, @@ -1720,25 +1962,22 @@ mod tests { let block_ids = writer.insert(vec![ BlockProperties { style: BlockStyle::Fixed, - position: buffer_snapshot.anchor_after(Point::new(1, 0)), + placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 0))), height: 1, - disposition: BlockDisposition::Above, render: Box::new(|_| div().into_any()), priority: 0, }, BlockProperties { style: BlockStyle::Fixed, - position: buffer_snapshot.anchor_after(Point::new(1, 2)), + placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 2))), height: 2, - disposition: BlockDisposition::Above, render: Box::new(|_| div().into_any()), priority: 0, }, BlockProperties { style: BlockStyle::Fixed, - position: buffer_snapshot.anchor_after(Point::new(3, 3)), + placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(3, 3))), height: 3, - disposition: BlockDisposition::Below, render: Box::new(|_| div().into_any()), priority: 0, }, @@ -1832,16 +2071,14 @@ mod tests { writer.insert(vec![ BlockProperties { style: BlockStyle::Fixed, - position: buffer_snapshot.anchor_after(Point::new(1, 12)), - disposition: BlockDisposition::Above, + placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 12))), render: Box::new(|_| div().into_any()), height: 1, priority: 0, }, BlockProperties { style: BlockStyle::Fixed, - position: buffer_snapshot.anchor_after(Point::new(1, 1)), - disposition: BlockDisposition::Below, + placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(1, 1))), render: Box::new(|_| div().into_any()), height: 1, priority: 0, @@ -1857,6 +2094,127 @@ mod tests { ); } + #[gpui::test] + fn test_replace_lines(cx: &mut gpui::TestAppContext) { + cx.update(init_test); + + let text = "line1\nline2\nline3\nline4\nline5"; + + let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); + let buffer_subscription = buffer.update(cx, |buffer, _cx| buffer.subscribe()); + let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx)); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); + let tab_size = 1.try_into().unwrap(); + let (mut tab_map, tab_snapshot) = CharMap::new(fold_snapshot, tab_size); + let (wrap_map, wraps_snapshot) = + cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx)); + let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0); + + let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); + 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)), + ), + height: 4, + render: Box::new(|_| div().into_any()), + priority: 0, + }]); + + let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); + assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5"); + + let buffer_snapshot = buffer.update(cx, |buffer, cx| { + buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx); + buffer.snapshot(cx) + }); + let (inlay_snapshot, inlay_edits) = inlay_map.sync( + buffer_snapshot.clone(), + buffer_subscription.consume().into_inner(), + ); + 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 blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits); + assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5"); + + let buffer_snapshot = buffer.update(cx, |buffer, cx| { + buffer.edit( + [( + Point::new(1, 5)..Point::new(1, 5), + "\nline 6\nline7\nline 8\nline 9", + )], + None, + cx, + ); + buffer.snapshot(cx) + }); + let (inlay_snapshot, inlay_edits) = inlay_map.sync( + buffer_snapshot.clone(), + buffer_subscription.consume().into_inner(), + ); + 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 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. + 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(1, 3))), + height: 1, + render: Box::new(|_| div().into_any()), + priority: 0, + }, + BlockProperties { + style: BlockStyle::Fixed, + placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(6, 2))), + height: 1, + render: Box::new(|_| div().into_any()), + priority: 0, + }, + ]); + let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); + assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\n\n\nline5"); + + // Ensure blocks inserted *inside* replaced region are hidden. + let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); + writer.insert(vec![ + BlockProperties { + style: BlockStyle::Fixed, + placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(1, 3))), + height: 1, + render: Box::new(|_| div().into_any()), + priority: 0, + }, + BlockProperties { + style: BlockStyle::Fixed, + placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(2, 1))), + height: 1, + render: Box::new(|_| div().into_any()), + priority: 0, + }, + BlockProperties { + style: BlockStyle::Fixed, + placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(6, 1))), + height: 1, + render: Box::new(|_| div().into_any()), + 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"); + } + #[gpui::test(iterations = 100)] fn test_random_blocks(cx: &mut gpui::TestAppContext, mut rng: StdRng) { cx.update(init_test); @@ -1879,14 +2237,21 @@ mod tests { log::info!("Wrap width: {:?}", wrap_width); log::info!("Excerpt Header Height: {:?}", excerpt_header_height); log::info!("Excerpt Footer Height: {:?}", excerpt_footer_height); - - let buffer = if rng.gen() { + let is_singleton = rng.gen(); + let buffer = if is_singleton { let len = rng.gen_range(0..10); let text = RandomCharIter::new(&mut rng).take(len).collect::(); - log::info!("initial buffer text: {:?}", text); + log::info!("initial singleton buffer text: {:?}", text); cx.update(|cx| MultiBuffer::build_simple(&text, cx)) } else { - cx.update(|cx| MultiBuffer::build_random(&mut rng, cx)) + cx.update(|cx| { + let multibuffer = MultiBuffer::build_random(&mut rng, cx); + log::info!( + "initial multi-buffer text: {:?}", + multibuffer.read(cx).read(cx).text() + ); + multibuffer + }) }; let mut buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx)); @@ -1902,7 +2267,6 @@ mod tests { excerpt_header_height, excerpt_footer_height, ); - let mut custom_blocks = Vec::new(); for _ in 0..operations { let mut buffer_edits = Vec::new(); @@ -1921,27 +2285,33 @@ mod tests { let block_properties = (0..block_count) .map(|_| { let buffer = cx.update(|cx| buffer.read(cx).read(cx).clone()); - let position = buffer.anchor_after( - buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left), - ); - - let disposition = if rng.gen() { - BlockDisposition::Above - } else { - BlockDisposition::Below + let offset = + buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left); + let mut min_height = 0; + let placement = match rng.gen_range(0..3) { + 0 => { + min_height = 1; + let start = buffer.anchor_after(offset); + let end = buffer.anchor_after(buffer.clip_offset( + rng.gen_range(offset..=buffer.len()), + Bias::Left, + )); + BlockPlacement::Replace(start..end) + } + 1 => BlockPlacement::Above(buffer.anchor_after(offset)), + _ => BlockPlacement::Below(buffer.anchor_after(offset)), }; - let height = rng.gen_range(0..5); + + let height = rng.gen_range(min_height..5); log::info!( - "inserting block {:?} {:?} with height {}", - disposition, - position.to_point(&buffer), + "inserting block {:?} with height {}", + placement.as_ref().map(|p| p.to_point(&buffer)), height ); BlockProperties { style: BlockStyle::Fixed, - position, + placement, height, - disposition, render: Box::new(|_| div().into_any()), priority: 0, } @@ -1957,28 +2327,21 @@ mod tests { wrap_map.sync(char_snapshot, tab_edits, cx) }); let mut block_map = block_map.write(wraps_snapshot, wrap_edits); - let block_ids = - block_map.insert(block_properties.iter().map(|props| BlockProperties { - position: props.position, - height: props.height, - style: props.style, - render: Box::new(|_| div().into_any()), - disposition: props.disposition, - priority: 0, - })); - for (block_id, props) in block_ids.into_iter().zip(block_properties) { - custom_blocks.push((block_id, props)); - } + block_map.insert(block_properties.iter().map(|props| BlockProperties { + placement: props.placement.clone(), + height: props.height, + style: props.style, + render: Box::new(|_| div().into_any()), + priority: 0, + })); } - 40..=59 if !custom_blocks.is_empty() => { - let block_count = rng.gen_range(1..=4.min(custom_blocks.len())); - let block_ids_to_remove = (0..block_count) - .map(|_| { - custom_blocks - .remove(rng.gen_range(0..custom_blocks.len())) - .0 - }) - .collect(); + 40..=59 if !block_map.custom_blocks.is_empty() => { + let block_count = rng.gen_range(1..=4.min(block_map.custom_blocks.len())); + let block_ids_to_remove = block_map + .custom_blocks + .choose_multiple(&mut rng, block_count) + .map(|block| block.id) + .collect::>(); let (inlay_snapshot, inlay_edits) = inlay_map.sync(buffer_snapshot.clone(), vec![]); @@ -2015,47 +2378,39 @@ mod tests { blocks_snapshot.transforms.summary().input_rows, wraps_snapshot.max_point().row() + 1 ); + log::info!("wrapped text: {:?}", wraps_snapshot.text()); log::info!("blocks text: {:?}", blocks_snapshot.text()); let mut expected_blocks = Vec::new(); - expected_blocks.extend(custom_blocks.iter().map(|(id, block)| { - let mut position = block.position.to_point(&buffer_snapshot); - match block.disposition { - BlockDisposition::Above => { - position.column = 0; - } - BlockDisposition::Below => { - position.column = buffer_snapshot.line_len(MultiBufferRow(position.row)); - } - }; - let row = wraps_snapshot.make_wrap_point(position, Bias::Left).row(); - ( - row, - ExpectedBlock::Custom { - disposition: block.disposition, - id: *id, - height: block.height, - priority: block.priority, - }, - ) + expected_blocks.extend(block_map.custom_blocks.iter().filter_map(|block| { + Some(( + block.placement.to_wrap_row(&wraps_snapshot)?, + Block::Custom(block.clone()), + )) })); // Note that this needs to be synced with the related section in BlockMap::sync - expected_blocks.extend( - BlockMap::header_and_footer_blocks( - true, - excerpt_footer_height, - buffer_start_header_height, - excerpt_header_height, - &buffer_snapshot, - 0.., - &wraps_snapshot, - ) - .map(|(row, block)| (row, block.into())), - ); + expected_blocks.extend(BlockMap::header_and_footer_blocks( + true, + excerpt_footer_height, + buffer_start_header_height, + excerpt_header_height, + &buffer_snapshot, + 0.., + &wraps_snapshot, + )); BlockMap::sort_blocks(&mut expected_blocks); + for (placement, block) in &expected_blocks { + log::info!( + "Block {:?} placement: {:?} Height: {:?}", + block.id(), + placement, + block.height() + ); + } + let mut sorted_blocks_iter = expected_blocks.into_iter().peekable(); let input_buffer_rows = buffer_snapshot @@ -2065,49 +2420,97 @@ mod tests { let mut expected_text = String::new(); let mut expected_block_positions = Vec::new(); let input_text = wraps_snapshot.text(); - for (row, input_line) in input_text.split('\n').enumerate() { - let row = row as u32; - if row > 0 { - expected_text.push('\n'); - } - let buffer_row = input_buffer_rows[wraps_snapshot - .to_point(WrapPoint::new(row, 0), Bias::Left) - .row as usize]; + // Loop over the input lines, creating (N - 1) empty lines for + // blocks of height N. + // + // It's important to note that output *starts* as one empty line, + // so we special case row 0 to assume a leading '\n'. + // + // Linehood is the birthright of strings. + let mut input_text_lines = input_text.split('\n').enumerate().peekable(); + let mut block_row = 0; + while let Some((wrap_row, input_line)) = input_text_lines.next() { + let wrap_row = wrap_row as u32; - while let Some((block_row, block)) = sorted_blocks_iter.peek() { - if *block_row == row && block.disposition() == BlockDisposition::Above { + // Create empty lines for the above block + while let Some((placement, block)) = sorted_blocks_iter.peek() { + if placement.start().0 == wrap_row && block.place_above() { let (_, block) = sorted_blocks_iter.next().unwrap(); - let height = block.height() as usize; - expected_block_positions - .push((expected_text.matches('\n').count() as u32, block)); - let text = "\n".repeat(height); - expected_text.push_str(&text); - for _ in 0..height { - expected_buffer_rows.push(None); + expected_block_positions.push((block_row, block.id())); + if block.height() > 0 { + let text = "\n".repeat((block.height() - 1) as usize); + if block_row > 0 { + expected_text.push('\n') + } + expected_text.push_str(&text); + for _ in 0..block.height() { + expected_buffer_rows.push(None); + } + block_row += block.height(); } } else { break; } } - let soft_wrapped = wraps_snapshot - .to_char_point(WrapPoint::new(row, 0)) - .column() - > 0; - expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row }); - expected_text.push_str(input_line); + // Skip lines within replace blocks, then create empty lines for the replace block's height + let mut is_in_replace_block = false; + if let Some((BlockPlacement::Replace(replace_range), block)) = + sorted_blocks_iter.peek() + { + if wrap_row >= replace_range.start.0 { + is_in_replace_block = true; + if wrap_row == replace_range.end.0 { + expected_block_positions.push((block_row, block.id())); + if block.height() > 0 { + let text = "\n".repeat((block.height() - 1) as usize); + if block_row > 0 { + expected_text.push('\n'); + } + expected_text.push_str(&text); + for _ in 0..block.height() { + expected_buffer_rows.push(None); + } + block_row += block.height(); + } - while let Some((block_row, block)) = sorted_blocks_iter.peek() { - if *block_row == row && block.disposition() == BlockDisposition::Below { + sorted_blocks_iter.next(); + } + } + } + + if !is_in_replace_block { + let buffer_row = input_buffer_rows[wraps_snapshot + .to_point(WrapPoint::new(wrap_row, 0), Bias::Left) + .row as usize]; + + let soft_wrapped = wraps_snapshot + .to_char_point(WrapPoint::new(wrap_row, 0)) + .column() + > 0; + expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row }); + if block_row > 0 { + expected_text.push('\n'); + } + expected_text.push_str(input_line); + block_row += 1; + } + + while let Some((placement, block)) = sorted_blocks_iter.peek() { + if placement.end().0 == wrap_row && block.place_below() { let (_, block) = sorted_blocks_iter.next().unwrap(); - let height = block.height() as usize; - expected_block_positions - .push((expected_text.matches('\n').count() as u32 + 1, block)); - let text = "\n".repeat(height); - expected_text.push_str(&text); - for _ in 0..height { - expected_buffer_rows.push(None); + expected_block_positions.push((block_row, block.id())); + if block.height() > 0 { + let text = "\n".repeat((block.height() - 1) as usize); + if block_row > 0 { + expected_text.push('\n') + } + expected_text.push_str(&text); + for _ in 0..block.height() { + expected_buffer_rows.push(None); + } + block_row += block.height(); } } else { break; @@ -2117,11 +2520,24 @@ mod tests { let expected_lines = expected_text.split('\n').collect::>(); let expected_row_count = expected_lines.len(); + + assert_eq!( + blocks_snapshot.max_point().row + 1, + expected_row_count as u32 + ); + + log::info!("expected text: {:?}", expected_text); + for start_row in 0..expected_row_count { - let expected_text = expected_lines[start_row..].join("\n"); + let end_row = rng.gen_range(start_row + 1..=expected_row_count); + let mut expected_text = expected_lines[start_row..end_row].join("\n"); + if end_row < expected_row_count { + expected_text.push('\n'); + } + let actual_text = blocks_snapshot .chunks( - start_row as u32..blocks_snapshot.max_point().row + 1, + start_row as u32..end_row as u32, false, false, Highlights::default(), @@ -2129,9 +2545,10 @@ mod tests { .map(|chunk| chunk.text) .collect::(); assert_eq!( - actual_text, expected_text, - "incorrect text starting from row {}", - start_row + actual_text, + expected_text, + "incorrect text starting row row range {:?}", + start_row..end_row ); assert_eq!( blocks_snapshot @@ -2145,7 +2562,7 @@ mod tests { assert_eq!( blocks_snapshot .blocks_in_range(0..(expected_row_count as u32)) - .map(|(row, block)| (row, block.clone().into())) + .map(|(row, block)| (row, block.id())) .collect::>(), expected_block_positions, "invalid blocks_in_range({:?})", @@ -2162,8 +2579,8 @@ mod tests { ); } - for (block_row, block) in expected_block_positions { - if let BlockType::Custom(block_id) = block.block_type() { + for (block_row, block_id) in expected_block_positions { + if let BlockId::Custom(block_id) = block_id { assert_eq!( blocks_snapshot.row_for_block(block_id), Some(BlockRow(block_row)) @@ -2204,10 +2621,12 @@ mod tests { longest_line_len, ); + // Ensure that conversion between block points and wrap points is stable. for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() { - let wrap_point = WrapPoint::new(row, 0); - let block_point = blocks_snapshot.to_block_point(wrap_point); - assert_eq!(blocks_snapshot.to_wrap_point(block_point), wrap_point); + let original_wrap_point = WrapPoint::new(row, 0); + let block_point = blocks_snapshot.to_block_point(original_wrap_point); + let wrap_point = blocks_snapshot.to_wrap_point(block_point); + assert_eq!(blocks_snapshot.to_block_point(wrap_point), block_point); } let mut block_point = BlockPoint::new(0, 0); @@ -2216,7 +2635,9 @@ mod tests { let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left); assert_eq!( blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)), - left_point + left_point, + "wrap point: {:?}", + blocks_snapshot.to_wrap_point(left_point) ); assert_eq!( left_buffer_point, @@ -2229,7 +2650,9 @@ mod tests { let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right); assert_eq!( blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)), - right_point + right_point, + "wrap point: {:?}", + blocks_snapshot.to_wrap_point(right_point) ); assert_eq!( right_buffer_point, @@ -2245,86 +2668,6 @@ mod tests { } } } - - #[derive(Debug, Eq, PartialEq)] - enum ExpectedBlock { - ExcerptBoundary { - height: u32, - starts_new_buffer: bool, - is_last: bool, - }, - Custom { - disposition: BlockDisposition, - id: CustomBlockId, - height: u32, - priority: usize, - }, - } - - impl BlockLike for ExpectedBlock { - fn block_type(&self) -> BlockType { - match self { - ExpectedBlock::Custom { id, .. } => BlockType::Custom(*id), - ExpectedBlock::ExcerptBoundary { .. } => BlockType::ExcerptBoundary, - } - } - - fn disposition(&self) -> BlockDisposition { - self.disposition() - } - - fn priority(&self) -> usize { - match self { - ExpectedBlock::Custom { priority, .. } => *priority, - ExpectedBlock::ExcerptBoundary { .. } => usize::MAX, - } - } - } - - impl ExpectedBlock { - fn height(&self) -> u32 { - match self { - ExpectedBlock::ExcerptBoundary { height, .. } => *height, - ExpectedBlock::Custom { height, .. } => *height, - } - } - - fn disposition(&self) -> BlockDisposition { - match self { - ExpectedBlock::ExcerptBoundary { is_last, .. } => { - if *is_last { - BlockDisposition::Below - } else { - BlockDisposition::Above - } - } - ExpectedBlock::Custom { disposition, .. } => *disposition, - } - } - } - - impl From for ExpectedBlock { - fn from(block: Block) -> Self { - match block { - Block::Custom(block) => ExpectedBlock::Custom { - id: block.id, - disposition: block.disposition, - height: block.height, - priority: block.priority, - }, - Block::ExcerptBoundary { - height, - starts_new_buffer, - next_excerpt, - .. - } => ExpectedBlock::ExcerptBoundary { - height, - starts_new_buffer, - is_last: next_excerpt.is_none(), - }, - } - } - } } fn init_test(cx: &mut gpui::AppContext) { diff --git a/crates/editor/src/display_map/char_map.rs b/crates/editor/src/display_map/char_map.rs index 443f8199a6..8c467b1803 100644 --- a/crates/editor/src/display_map/char_map.rs +++ b/crates/editor/src/display_map/char_map.rs @@ -252,6 +252,7 @@ impl CharSnapshot { }; TabChunks { + snapshot: self, fold_chunks: self.fold_snapshot.chunks( input_start..input_end, language_aware, @@ -492,6 +493,7 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary { const SPACES: &str = " "; pub struct TabChunks<'a> { + snapshot: &'a CharSnapshot, fold_chunks: FoldChunks<'a>, chunk: Chunk<'a>, column: u32, @@ -503,6 +505,37 @@ pub struct TabChunks<'a> { inside_leading_tab: bool, } +impl<'a> TabChunks<'a> { + pub(crate) fn seek(&mut self, range: Range) { + let (input_start, expanded_char_column, to_next_stop) = + self.snapshot.to_fold_point(range.start, Bias::Left); + let input_column = input_start.column(); + let input_start = input_start.to_offset(&self.snapshot.fold_snapshot); + let input_end = self + .snapshot + .to_fold_point(range.end, Bias::Right) + .0 + .to_offset(&self.snapshot.fold_snapshot); + let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 { + range.end.column() - range.start.column() + } else { + to_next_stop + }; + + self.fold_chunks.seek(input_start..input_end); + self.input_column = input_column; + self.column = expanded_char_column; + self.output_position = range.start.0; + self.max_output_position = range.end.0; + self.chunk = Chunk { + text: &SPACES[0..(to_next_stop as usize)], + is_tab: true, + ..Default::default() + }; + self.inside_leading_tab = to_next_stop > 0; + } +} + impl<'a> Iterator for TabChunks<'a> { type Item = Chunk<'a>; diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 5eb26ff969..2cfe4b41f5 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1100,6 +1100,17 @@ pub struct FoldBufferRows<'a> { fold_point: FoldPoint, } +impl<'a> FoldBufferRows<'a> { + pub(crate) fn seek(&mut self, row: u32) { + let fold_point = FoldPoint::new(row, 0); + self.cursor.seek(&fold_point, Bias::Left, &()); + let overshoot = fold_point.0 - self.cursor.start().0 .0; + let inlay_point = InlayPoint(self.cursor.start().1 .0 + overshoot); + self.input_buffer_rows.seek(inlay_point.row()); + self.fold_point = fold_point; + } +} + impl<'a> Iterator for FoldBufferRows<'a> { type Item = Option; @@ -1135,6 +1146,38 @@ pub struct FoldChunks<'a> { max_output_offset: FoldOffset, } +impl<'a> FoldChunks<'a> { + pub(crate) fn seek(&mut self, range: Range) { + self.transform_cursor.seek(&range.start, Bias::Right, &()); + + let inlay_start = { + let overshoot = range.start.0 - self.transform_cursor.start().0 .0; + self.transform_cursor.start().1 + InlayOffset(overshoot) + }; + + let transform_end = self.transform_cursor.end(&()); + + let inlay_end = if self + .transform_cursor + .item() + .map_or(true, |transform| transform.is_fold()) + { + inlay_start + } else if range.end < transform_end.0 { + let overshoot = range.end.0 - self.transform_cursor.start().0 .0; + self.transform_cursor.start().1 + InlayOffset(overshoot) + } else { + transform_end.1 + }; + + self.inlay_chunks.seek(inlay_start..inlay_end); + self.inlay_chunk = None; + self.inlay_offset = inlay_start; + self.output_offset = range.start; + self.max_output_offset = range.end; + } +} + impl<'a> Iterator for FoldChunks<'a> { type Item = Chunk<'a>; diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index f418f45fec..15f6595f19 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -56,6 +56,7 @@ pub struct WrapChunks<'a> { output_position: WrapPoint, max_output_row: u32, transforms: Cursor<'a, Transform, (WrapPoint, CharPoint)>, + snapshot: &'a WrapSnapshot, } #[derive(Clone)] @@ -68,6 +69,21 @@ pub struct WrapBufferRows<'a> { transforms: Cursor<'a, Transform, (WrapPoint, CharPoint)>, } +impl<'a> WrapBufferRows<'a> { + pub(crate) fn seek(&mut self, start_row: u32) { + self.transforms + .seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); + let mut input_row = self.transforms.start().1.row(); + if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { + input_row += start_row - self.transforms.start().0.row(); + } + self.soft_wrapped = self.transforms.item().map_or(false, |t| !t.is_isomorphic()); + self.input_buffer_rows.seek(input_row); + self.input_buffer_row = self.input_buffer_rows.next().unwrap(); + self.output_row = start_row; + } +} + impl WrapMap { pub fn new( char_snapshot: CharSnapshot, @@ -602,6 +618,7 @@ impl WrapSnapshot { output_position: output_start, max_output_row: rows.end, transforms, + snapshot: self, } } @@ -629,6 +646,67 @@ impl WrapSnapshot { } } + pub fn text_summary_for_range(&self, rows: Range) -> TextSummary { + let mut summary = TextSummary::default(); + + let start = WrapPoint::new(rows.start, 0); + let end = WrapPoint::new(rows.end, 0); + + let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&()); + cursor.seek(&start, Bias::Right, &()); + if let Some(transform) = cursor.item() { + let start_in_transform = start.0 - cursor.start().0 .0; + let end_in_transform = cmp::min(end, cursor.end(&()).0).0 - cursor.start().0 .0; + if transform.is_isomorphic() { + let char_start = CharPoint(cursor.start().1 .0 + start_in_transform); + let char_end = CharPoint(cursor.start().1 .0 + end_in_transform); + summary += &self + .char_snapshot + .text_summary_for_range(char_start..char_end); + } else { + debug_assert_eq!(start_in_transform.row, end_in_transform.row); + let indent_len = end_in_transform.column - start_in_transform.column; + summary += &TextSummary { + lines: Point::new(0, indent_len), + first_line_chars: indent_len, + last_line_chars: indent_len, + longest_row: 0, + longest_row_chars: indent_len, + }; + } + + cursor.next(&()); + } + + if rows.end > cursor.start().0.row() { + summary += &cursor + .summary::<_, TransformSummary>(&WrapPoint::new(rows.end, 0), Bias::Right, &()) + .output; + + if let Some(transform) = cursor.item() { + let end_in_transform = end.0 - cursor.start().0 .0; + if transform.is_isomorphic() { + let char_start = cursor.start().1; + let char_end = CharPoint(char_start.0 + end_in_transform); + summary += &self + .char_snapshot + .text_summary_for_range(char_start..char_end); + } else { + debug_assert_eq!(end_in_transform, Point::new(1, 0)); + summary += &TextSummary { + lines: Point::new(1, 0), + first_line_chars: 0, + last_line_chars: 0, + longest_row: 0, + longest_row_chars: 0, + }; + } + } + } + + summary + } + pub fn soft_wrap_indent(&self, row: u32) -> Option { let mut cursor = self.transforms.cursor::(&()); cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right, &()); @@ -745,6 +823,21 @@ impl WrapSnapshot { None } + #[cfg(test)] + pub fn text(&self) -> String { + self.text_chunks(0).collect() + } + + #[cfg(test)] + pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { + self.chunks( + wrap_row..self.max_point().row() + 1, + false, + Highlights::default(), + ) + .map(|h| h.text) + } + fn check_invariants(&self) { #[cfg(test)] { @@ -791,6 +884,26 @@ impl WrapSnapshot { } } +impl<'a> WrapChunks<'a> { + pub(crate) fn seek(&mut self, rows: Range) { + let output_start = WrapPoint::new(rows.start, 0); + let output_end = WrapPoint::new(rows.end, 0); + self.transforms.seek(&output_start, Bias::Right, &()); + let mut input_start = CharPoint(self.transforms.start().1 .0); + if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { + input_start.0 += output_start.0 - self.transforms.start().0 .0; + } + let input_end = self + .snapshot + .to_char_point(output_end) + .min(self.snapshot.char_snapshot.max_point()); + self.input_chunks.seek(input_start..input_end); + self.input_chunk = Chunk::default(); + self.output_position = output_start; + self.max_output_row = rows.end; + } +} + impl<'a> Iterator for WrapChunks<'a> { type Item = Chunk<'a>; @@ -1336,19 +1449,6 @@ mod tests { } impl WrapSnapshot { - pub fn text(&self) -> String { - self.text_chunks(0).collect() - } - - pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { - self.chunks( - wrap_row..self.max_point().row() + 1, - false, - Highlights::default(), - ) - .map(|h| h.text) - } - fn verify_chunks(&mut self, rng: &mut impl Rng) { for _ in 0..5 { let mut end_row = rng.gen_range(0..=self.max_point().row()); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d3e2134eac..f3fb5cd360 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10210,7 +10210,7 @@ impl Editor { let block_id = this.insert_blocks( [BlockProperties { style: BlockStyle::Flex, - position: range.start, + placement: BlockPlacement::Below(range.start), height: 1, render: Box::new({ let rename_editor = rename_editor.clone(); @@ -10246,7 +10246,6 @@ impl Editor { .into_any_element() } }), - disposition: BlockDisposition::Below, priority: 0, }], Some(Autoscroll::fit()), @@ -10531,10 +10530,11 @@ impl Editor { let message_height = diagnostic.message.matches('\n').count() as u32 + 1; BlockProperties { style: BlockStyle::Fixed, - position: buffer.anchor_after(entry.range.start), + placement: BlockPlacement::Below( + buffer.anchor_after(entry.range.start), + ), height: message_height, render: diagnostic_block_renderer(diagnostic, None, true, true), - disposition: BlockDisposition::Below, priority: 0, } }), diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index fdcfaab82f..99b5cb6637 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -3868,8 +3868,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { editor.insert_blocks( [BlockProperties { style: BlockStyle::Fixed, - position: snapshot.anchor_after(Point::new(2, 0)), - disposition: BlockDisposition::Below, + placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))), height: 1, render: Box::new(|_| div().into_any()), priority: 0, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5a356965a4..753b7f246d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2071,7 +2071,7 @@ impl EditorElement { let mut element = match block { Block::Custom(block) => { let align_to = block - .position() + .start() .to_point(&snapshot.buffer_snapshot) .to_display_point(snapshot); let anchor_x = text_x @@ -6294,7 +6294,7 @@ fn compute_auto_height_layout( mod tests { use super::*; use crate::{ - display_map::{BlockDisposition, BlockProperties}, + display_map::{BlockPlacement, BlockProperties}, editor_tests::{init_test, update_test_language_settings}, Editor, MultiBuffer, }; @@ -6550,9 +6550,8 @@ mod tests { editor.insert_blocks( [BlockProperties { style: BlockStyle::Fixed, - disposition: BlockDisposition::Above, + placement: BlockPlacement::Above(Anchor::min()), height: 3, - position: Anchor::min(), render: Box::new(|cx| div().h(3. * cx.line_height()).into_any()), priority: 0, }], diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 1b9408df7e..9f66d27a64 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -17,7 +17,7 @@ use workspace::Item; use crate::{ editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyDiffHunk, - BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow, + BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff, }; @@ -417,10 +417,9 @@ impl Editor { }; BlockProperties { - position: hunk.multi_buffer_range.start, + placement: BlockPlacement::Above(hunk.multi_buffer_range.start), height: 1, style: BlockStyle::Sticky, - disposition: BlockDisposition::Above, priority: 0, render: Box::new({ let editor = cx.view().clone(); @@ -700,10 +699,9 @@ impl Editor { let hunk = hunk.clone(); let height = editor_height.max(deleted_text_height); BlockProperties { - position: hunk.multi_buffer_range.start, + placement: BlockPlacement::Above(hunk.multi_buffer_range.start), height, style: BlockStyle::Flex, - disposition: BlockDisposition::Above, priority: 0, render: Box::new(move |cx| { let width = EditorElement::diff_hunk_strip_width(cx.line_height()); diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index 7f312023c3..2eba678fde 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -8,7 +8,7 @@ use client::telemetry::Telemetry; use collections::{HashMap, HashSet}; use editor::{ display_map::{ - BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, CustomBlockId, + BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, }, scroll::Autoscroll, @@ -90,12 +90,11 @@ impl EditorBlock { let invalidation_anchor = buffer.read(cx).read(cx).anchor_before(next_row_start); let block = BlockProperties { - position: code_range.end, + placement: BlockPlacement::Below(code_range.end), // Take up at least one height for status, allow the editor to determine the real height based on the content from render height: 1, style: BlockStyle::Sticky, render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()), - disposition: BlockDisposition::Below, priority: 0, };