From 08a3c54bacc270395e7f1ac53d0963d9d71b8aea Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Oct 2024 03:29:25 -0700 Subject: [PATCH] Allow editor blocks to replace ranges of text (#19531) This PR adds the ability for editor blocks to replace lines of text, but does not yet use that feature anywhere. We'll update assistant patches to use replace blocks on another branch: https://github.com/zed-industries/zed/tree/assistant-patch-replace-blocks Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra Co-authored-by: Richard Feldman Co-authored-by: Marshall Bowers Co-authored-by: Nathan Sobo --- crates/assistant/src/assistant_panel.rs | 27 +- crates/assistant/src/inline_assistant.rs | 11 +- crates/diagnostics/src/diagnostics.rs | 28 +- crates/editor/src/display_map.rs | 19 +- crates/editor/src/display_map/block_map.rs | 1399 ++++++++++++-------- crates/editor/src/display_map/char_map.rs | 33 + crates/editor/src/display_map/fold_map.rs | 43 + crates/editor/src/display_map/wrap_map.rs | 126 +- crates/editor/src/editor.rs | 8 +- crates/editor/src/editor_tests.rs | 3 +- crates/editor/src/element.rs | 7 +- crates/editor/src/hunk_diff.rs | 8 +- crates/repl/src/session.rs | 5 +- 13 files changed, 1118 insertions(+), 599 deletions(-) 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, };