From 6e0f65e05afd542619e0ac6555a66d930926715d Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 29 Oct 2025 21:30:16 -0400 Subject: [PATCH] sketch in split editor and start working on new logic in block map Co-authored-by: cameron --- crates/editor/src/display_map/block_map.rs | 115 +++++++++++++++--- crates/editor/src/display_map/wrap_map.rs | 9 +- crates/editor/src/element.rs | 10 ++ crates/editor/src/split.rs | 130 +++++++++++++++++++++ crates/editor/src/test.rs | 5 + 5 files changed, 250 insertions(+), 19 deletions(-) create mode 100644 crates/editor/src/split.rs diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 99234899d3af7505c911355e34abe8f3fea3d0d2..daf0cec397fc2271ba45153d099011e32a581453 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -257,6 +257,16 @@ pub enum BlockId { ExcerptBoundary(ExcerptId), FoldedBuffer(ExcerptId), Custom(CustomBlockId), + Spacer(SpacerId), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub struct SpacerId(pub usize); + +impl From for EntityId { + fn from(value: SpacerId) -> Self { + EntityId::from(value.0 as u64) + } } impl From for ElementId { @@ -267,6 +277,7 @@ impl From for ElementId { ("ExcerptBoundary", EntityId::from(excerpt_id)).into() } BlockId::FoldedBuffer(id) => ("FoldedBuffer", EntityId::from(id)).into(), + BlockId::Spacer(id) => ("Spacer", EntityId::from(id)).into(), } } } @@ -277,6 +288,7 @@ impl std::fmt::Display for BlockId { Self::Custom(id) => write!(f, "Block({id:?})"), Self::ExcerptBoundary(id) => write!(f, "ExcerptHeader({id:?})"), Self::FoldedBuffer(id) => write!(f, "FoldedBuffer({id:?})"), + Self::Spacer(id) => write!(f, "Spacer({id:?})"), } } } @@ -302,6 +314,10 @@ pub enum Block { excerpt: ExcerptInfo, height: u32, }, + Spacer { + height: u32, + id: SpacerId, + }, } impl Block { @@ -317,6 +333,7 @@ impl Block { excerpt: next_excerpt, .. } => BlockId::ExcerptBoundary(next_excerpt.id), + Block::Spacer { id, .. } => BlockId::Spacer(*id), } } @@ -325,7 +342,8 @@ impl Block { Block::Custom(block) => block.height.is_some(), Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } - | Block::BufferHeader { .. } => true, + | Block::BufferHeader { .. } + | Block::Spacer { .. } => true, } } @@ -334,7 +352,8 @@ impl Block { Block::Custom(block) => block.height.unwrap_or(0), Block::ExcerptBoundary { height, .. } | Block::FoldedBuffer { height, .. } - | Block::BufferHeader { height, .. } => *height, + | Block::BufferHeader { height, .. } + | Block::Spacer { height, .. } => *height, } } @@ -343,7 +362,8 @@ impl Block { Block::Custom(block) => block.style, Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } - | Block::BufferHeader { .. } => BlockStyle::Sticky, + | Block::BufferHeader { .. } + | Block::Spacer { .. } => BlockStyle::Sticky, } } @@ -353,6 +373,7 @@ impl Block { Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => true, Block::BufferHeader { .. } => true, + Block::Spacer { .. } => false, } } @@ -362,6 +383,7 @@ impl Block { Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => false, Block::BufferHeader { .. } => false, + Block::Spacer { .. } => false, } } @@ -374,6 +396,7 @@ impl Block { Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => false, Block::BufferHeader { .. } => false, + Block::Spacer { .. } => false, } } @@ -383,6 +406,7 @@ impl Block { Block::FoldedBuffer { .. } => true, Block::ExcerptBoundary { .. } => false, Block::BufferHeader { .. } => false, + Block::Spacer { .. } => true, } } @@ -392,6 +416,7 @@ impl Block { Block::FoldedBuffer { .. } => true, Block::ExcerptBoundary { .. } => true, Block::BufferHeader { .. } => true, + Block::Spacer { .. } => false, } } @@ -401,6 +426,7 @@ impl Block { Block::FoldedBuffer { .. } => true, Block::ExcerptBoundary { .. } => false, Block::BufferHeader { .. } => true, + Block::Spacer { .. } => false, } } } @@ -427,6 +453,11 @@ impl Debug for Block { .field("excerpt", excerpt) .field("height", height) .finish(), + Self::Spacer { height, id } => f + .debug_struct("Spacer") + .field("height", height) + .field("id", id) + .finish(), } } } @@ -540,8 +571,29 @@ impl BlockMap { let mut edits = edits.into_iter().peekable(); while let Some(edit) = edits.next() { - let mut old_start = WrapRow(edit.old.start); + // FIXME biases? + + let mut old_buffer_start = self + .wrap_snapshot + .borrow() + .to_point(WrapPoint::new(edit.old.start, 0), Bias::Left); + old_buffer_start.column = 0; + let mut old_start = WrapRow( + self.wrap_snapshot + .borrow() + .make_wrap_point(old_buffer_start, Bias::Left) + .row(), + ); + + let mut new_buffer_start = + wrap_snapshot.to_point(WrapPoint::new(edit.new.start, 0), Bias::Left); + new_buffer_start.column = 0; let mut new_start = WrapRow(edit.new.start); + let mut new_start = WrapRow( + wrap_snapshot + .make_wrap_point(new_buffer_start, Bias::Left) + .row(), + ); // Only preserve transforms that: // * Strictly precedes this edit @@ -694,6 +746,10 @@ impl BlockMap { BlockMap::sort_blocks(&mut blocks_in_edit); + let hunks = buffer + .diff_hunks_in_range(new_buffer_start..new_buffer_end) + .collect::>(); + // For each of these blocks, insert a new isomorphic transform preceding the block, // and then insert the block itself. let mut just_processed_folded_buffer = false; @@ -918,6 +974,19 @@ impl BlockMap { _ => false, }); } + + // fn spacer_blocks<'a, R, T>( + // &'a self, + // buffer: &'a multi_buffer::MultiBufferSnapshot, + // range: R, + // wrap_snapshot: &'a WrapSnapshot, + // ) -> impl Iterator, Block)> + 'a + // where + // R: RangeBounds, + // T: multi_buffer::ToOffset, + // { + // let start_row = range.start_bound().map(|x| x.to_offset(snapshot)) + // } } fn push_isomorphic(tree: &mut SumTree, rows: u32, wrap_snapshot: &WrapSnapshot) { @@ -1429,31 +1498,42 @@ impl BlockSnapshot { pub fn block_for_id(&self, block_id: BlockId) -> Option { let buffer = self.wrap_snapshot.buffer_snapshot(); - let wrap_point = match block_id { + let wrap_row = match block_id { BlockId::Custom(custom_block_id) => { let custom_block = self.custom_blocks_by_id.get(&custom_block_id)?; return Some(Block::Custom(custom_block.clone())); } BlockId::ExcerptBoundary(next_excerpt_id) => { let excerpt_range = buffer.range_for_excerpt(next_excerpt_id)?; - self.wrap_snapshot - .make_wrap_point(excerpt_range.start, Bias::Left) + Some( + self.wrap_snapshot + .make_wrap_point(excerpt_range.start, Bias::Left) + .row(), + ) } - BlockId::FoldedBuffer(excerpt_id) => self - .wrap_snapshot - .make_wrap_point(buffer.range_for_excerpt(excerpt_id)?.start, Bias::Left), + BlockId::FoldedBuffer(excerpt_id) => Some( + self.wrap_snapshot + .make_wrap_point(buffer.range_for_excerpt(excerpt_id)?.start, Bias::Left) + .row(), + ), + BlockId::Spacer(_) => None, }; - let wrap_row = WrapRow(wrap_point.row()); let mut cursor = self.transforms.cursor::(()); - cursor.seek(&wrap_row, Bias::Left); + if let Some(wrap_row) = wrap_row { + cursor.seek(&WrapRow(wrap_row), Bias::Left); + } else { + cursor.next(); + } while let Some(transform) = cursor.item() { if let Some(block) = transform.block.as_ref() { if block.id() == block_id { return Some(block.clone()); } - } else if *cursor.start() > wrap_row { + } else if let Some(wrap_row) = wrap_row + && *cursor.start() > WrapRow(wrap_row) + { break; } @@ -2211,10 +2291,10 @@ mod tests { let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); - let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx); + let (_, wrap_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx); - let block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1); - let snapshot = block_map.read(wraps_snapshot, Default::default()); + let block_map = BlockMap::new(wrap_snapshot.clone(), 1, 1); + let snapshot = block_map.read(wrap_snapshot.clone(), Default::default()); // Each excerpt has a header above and footer below. Excerpts are also *separated* by a newline. assert_eq!(snapshot.text(), "\nBuff\ner 1\n\nBuff\ner 2\n\nBuff\ner 3"); @@ -2231,6 +2311,9 @@ mod tests { (6..7, BlockId::ExcerptBoundary(excerpt_ids[2])), // path, header ] ); + + dbg!(wrap_snapshot.row_infos(0).collect::>()); + // dbg!(block_map.transforms.borrow().iter().collect::>()); } #[gpui::test] diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index e79e5555a61d0ddb8a93a1708c676554f191c3f6..11e1b92df0f84c61e6e7d76f141096abfc379e31 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -31,18 +31,21 @@ pub struct WrapMap { #[derive(Clone)] pub struct WrapSnapshot { pub(super) tab_snapshot: TabSnapshot, - transforms: SumTree, + // FIXME + pub(crate) transforms: SumTree, interpolated: bool, } +// FIXME #[derive(Clone, Debug, Default, Eq, PartialEq)] -struct Transform { +pub(crate) struct Transform { summary: TransformSummary, display_text: Option<&'static str>, } +// FIXME #[derive(Clone, Debug, Default, Eq, PartialEq)] -struct TransformSummary { +pub(crate) struct TransformSummary { input: TextSummary, output: TextSummary, } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 41a9809bfa75f091c1c03d924ffebf117d4fd2d7..70847b29eed47d0704410bbab0c4cc622fed6320 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3757,6 +3757,16 @@ impl EditorElement { result.into_any() } + + Block::Spacer { height, .. } => v_flex() + .id(block_id) + .w_full() + .child( + div() + .h(*height as f32 * window.line_height()) + .debug_bg_magenta(), + ) + .into_any(), }; // Discover the element's content height, then round up to the nearest multiple of line height. diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs new file mode 100644 index 0000000000000000000000000000000000000000..d6ed01515d2b8c84491c13d56a47946331c75ee4 --- /dev/null +++ b/crates/editor/src/split.rs @@ -0,0 +1,130 @@ +use crate::EditorEvent; + +use super::{Editor, EditorElement, EditorStyle}; +use gpui::{App, Context, Entity, Render, Window}; +use ui::{Element, IntoElement}; +use workspace::ItemHandle; + +// stage 1: render two side-by-side editors with the correct scrolling behavior +// stage 2: add alignment map to insert blank lines + +/// An editor that can be rendered with a split diff layout. +/// +/// When [secondary] is `None`, it is rendered with an inline diff style. +pub struct SplittableEditor { + primary: Entity, + secondary: Option>, +} + +impl SplittableEditor { + fn subscribe(&mut self, window: &mut Window, cx: &mut Context) { + cx.subscribe_in( + &self.primary, + window, + |this, editor, event: &EditorEvent, window, cx| {}, + ) + .detach(); + } + + fn sync_state(&mut self, cx: &mut App) {} +} + +impl SplittableEditor {} + +struct SplitEditorElement { + primary: Entity, + secondary: Entity, + style: EditorStyle, +} + +struct SplitEditorElementLayout {} + +impl Element for SplitEditorElement { + type RequestLayoutState = (); + + type PrepaintState = SplitEditorElementLayout; + + fn id(&self) -> Option { + todo!() + } + + fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + todo!() + } + + fn request_layout( + &mut self, + id: Option<&gpui::GlobalElementId>, + inspector_id: Option<&gpui::InspectorElementId>, + window: &mut ui::Window, + cx: &mut ui::App, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { + } + + fn prepaint( + &mut self, + id: Option<&gpui::GlobalElementId>, + inspector_id: Option<&gpui::InspectorElementId>, + bounds: gpui::Bounds, + request_layout: &mut Self::RequestLayoutState, + window: &mut ui::Window, + cx: &mut ui::App, + ) -> Self::PrepaintState { + todo!() + } + + fn paint( + &mut self, + id: Option<&gpui::GlobalElementId>, + inspector_id: Option<&gpui::InspectorElementId>, + bounds: gpui::Bounds, + request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + window: &mut ui::Window, + cx: &mut ui::App, + ) { + todo!() + } +} + +impl Render for SplittableEditor { + fn render( + &mut self, + window: &mut ui::Window, + cx: &mut ui::Context, + ) -> impl ui::IntoElement { + enum SplittableEditorElement { + Single(EditorElement), + Split(SplitEditorElement), + } + + impl Element for SplittableEditorElement {} + impl IntoElement for SplittableEditorElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } + } + + let style; + + if let Some(secondary) = self.secondary.clone() { + SplittableEditorElement::Split(SplitEditorElement { + primary: self.primary.clone(), + secondary, + style, + }) + } else { + SplittableEditorElement::Single(EditorElement::new(&self.primary.clone(), style)) + } + } +} + +impl IntoElement for SplitEditorElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 9d1003e8c08b3d725ffa13b90eb0ee405520d8cd..3d138dd63f79fb340f22343297ebd65b6e6216bc 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -238,6 +238,11 @@ pub fn editor_content_with_blocks(editor: &Entity, cx: &mut VisualTestCo lines[row as usize].push_str("ยง -----"); } } + Block::Spacer { height, .. } => { + for row in row.0..row.0 + height { + lines[row as usize].push_str("@@@@@@@"); + } + } } } lines.join("\n")