diff --git a/Cargo.lock b/Cargo.lock index a7f690078f21a..83472cf28317d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7702,11 +7702,13 @@ dependencies = [ "ctor", "env_logger 0.11.5", "futures 0.3.31", + "git", "gpui", "itertools 0.13.0", "language", "log", "parking_lot", + "project", "rand 0.8.5", "serde", "settings", diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 773ee97a9d2ed..35588a0911343 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -9,6 +9,7 @@ use editor::{ ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename, ToggleCodeActions, Undo, }, + display_map::RowInfo, test::editor_test_context::{AssertionContextManager, EditorTestContext}, Editor, }; @@ -20,7 +21,6 @@ use language::{ language_settings::{AllLanguageSettings, InlayHintSettings}, FakeLspAdapter, }; -use multi_buffer::MultiBufferRow; use project::{ project_settings::{InlineBlameSettings, ProjectSettings}, SERVER_PROGRESS_THROTTLE_TIMEOUT, @@ -2075,7 +2075,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA let blame = editor_b.blame().expect("editor_b should have blame now"); let entries = blame.update(cx, |blame, cx| { blame - .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(0..4) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + cx, + ) .collect::>() }); @@ -2114,7 +2122,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA let blame = editor_b.blame().expect("editor_b should have blame now"); let entries = blame.update(cx, |blame, cx| { blame - .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(0..4) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + cx, + ) .collect::>() }); @@ -2141,7 +2157,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA let blame = editor_b.blame().expect("editor_b should have blame now"); let entries = blame.update(cx, |blame, cx| { blame - .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(0..4) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + cx, + ) .collect::>() }); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 7ec189395673d..87ea4425e5bd2 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -20,6 +20,7 @@ mod block_map; mod crease_map; mod custom_highlights; +mod diff_map; mod fold_map; mod inlay_map; pub(crate) mod invisibles; @@ -30,16 +31,19 @@ use crate::{ hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt, }; pub use block_map::{ - Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, - BlockPlacement, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, + Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement, + BlockPoint, BlockProperties, BlockRows, BlockStyle, CustomBlockId, RenderBlock, }; use block_map::{BlockRow, BlockSnapshot}; use collections::{HashMap, HashSet}; pub use crease_map::*; +use diff_map::{DiffMap, DiffMapSnapshot, DiffOffset, DiffPoint}; pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint}; use fold_map::{FoldMap, FoldSnapshot}; +use git::diff::DiffHunkStatus; use gpui::{ - AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle, + AnyElement, AppContext, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, + UnderlineStyle, }; pub(crate) use inlay_map::Inlay; use inlay_map::{InlayMap, InlaySnapshot}; @@ -51,9 +55,10 @@ use language::{ }; use lsp::DiagnosticSeverity; use multi_buffer::{ - Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot, - ToOffset, ToPoint, + Anchor, AnchorRangeExt, MultiBuffer, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, + MultiBufferSnapshot, RowInfo, ToOffset, ToPoint, }; +use project::buffer_store::BufferChangeSet; use serde::Deserialize; use std::{ any::TypeId, @@ -66,7 +71,7 @@ use std::{ }; use sum_tree::{Bias, TreeMap}; use tab_map::{TabMap, TabSnapshot}; -use text::LineIndent; +use text::{BufferId, LineIndent}; use ui::{px, SharedString, WindowContext}; use unicode_segmentation::UnicodeSegmentation; use wrap_map::{WrapMap, WrapSnapshot}; @@ -96,6 +101,8 @@ pub struct DisplayMap { buffer_subscription: BufferSubscription, /// Decides where the [`Inlay`]s should be displayed. inlay_map: InlayMap, + /// Decides where diff hunks should be. + diff_map: Model, /// Decides where the fold indicators should be and tracks parts of a source file that are currently folded. fold_map: FoldMap, /// Keeps track of hard tabs in a buffer. @@ -134,7 +141,8 @@ impl DisplayMap { let tab_size = Self::tab_size(&buffer, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); let crease_map = CreaseMap::new(&buffer_snapshot); - let (inlay_map, snapshot) = InlayMap::new(buffer_snapshot); + let (diff_map, snapshot) = DiffMap::new(buffer.clone(), cx); + let (inlay_map, snapshot) = InlayMap::new(snapshot); let (fold_map, snapshot) = FoldMap::new(snapshot); let (tab_map, snapshot) = TabMap::new(snapshot, tab_size); let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx); @@ -153,6 +161,7 @@ impl DisplayMap { buffer_subscription, fold_map, inlay_map, + diff_map, tab_map, wrap_map, block_map, @@ -168,7 +177,10 @@ impl DisplayMap { pub fn snapshot(&mut self, cx: &mut ModelContext) -> DisplaySnapshot { let buffer_snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); - let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits); + let (diff_snapshot, edits) = self.diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer_snapshot.clone(), edits, cx) + }); + let (inlay_snapshot, edits) = self.inlay_map.sync(diff_snapshot, edits); let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits); let tab_size = Self::tab_size(&self.buffer, cx); let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size); @@ -217,7 +229,10 @@ impl DisplayMap { let buffer_snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); - let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot.clone(), edits); + let (snapshot, edits) = self.diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer_snapshot.clone(), edits, cx) + }); + let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); let (snapshot, edits) = self @@ -290,6 +305,9 @@ impl DisplayMap { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self + .diff_map + .update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx)); let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); @@ -319,6 +337,9 @@ impl DisplayMap { .collect::>(); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self + .diff_map + .update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx)); let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); @@ -341,6 +362,9 @@ impl DisplayMap { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self + .diff_map + .update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx)); let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); let (snapshot, edits) = self.fold_map.read(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); @@ -355,6 +379,9 @@ impl DisplayMap { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self + .diff_map + .update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx)); let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); let (snapshot, edits) = self.fold_map.read(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); @@ -387,6 +414,71 @@ impl DisplayMap { self.crease_map.remove(crease_ids, &snapshot) } + pub fn add_change_set( + &mut self, + change_set: Model, + cx: &mut ModelContext, + ) { + self.diff_map.update(cx, |diff_map, cx| { + diff_map.add_change_set(change_set, cx); + }); + } + + pub fn has_multiple_hunks(&self, cx: &AppContext) -> bool { + self.diff_map.read(cx).has_multiple_hunks() + } + + pub fn has_expanded_diff_hunks_in_ranges( + &mut self, + ranges: &[Range], + cx: &mut ModelContext, + ) -> bool { + self.diff_map + .read(cx) + .has_expanded_diff_hunks_in_ranges(ranges) + } + + pub fn set_all_hunks_expanded(&mut self, cx: &mut ModelContext) { + self.update_diff_map(cx, |diff_map, cx| diff_map.set_all_hunks_expanded(cx)) + } + + pub fn expand_diff_hunks(&mut self, ranges: Vec>, cx: &mut ModelContext) { + self.update_diff_map(cx, |diff_map, cx| diff_map.expand_diff_hunks(ranges, cx)) + } + + pub fn collapse_diff_hunks(&mut self, ranges: Vec>, cx: &mut ModelContext) { + self.update_diff_map(cx, |diff_map, cx| diff_map.collapse_diff_hunks(ranges, cx)) + } + + pub fn diff_base_for<'a>( + &'a self, + buffer_id: BufferId, + cx: &'a AppContext, + ) -> Option<&'a Model> { + self.diff_map.read(cx).diff_base_for(buffer_id) + } + + fn update_diff_map( + &mut self, + cx: &mut ModelContext, + f: impl FnOnce(&mut DiffMap, &mut ModelContext), + ) { + let snapshot = self.buffer.read(cx).snapshot(cx); + let edits = self.buffer_subscription.consume().into_inner(); + let (snapshot, edits) = self.diff_map.update(cx, |diff_map, cx| { + f(diff_map, cx); + diff_map.sync(snapshot, edits, cx) + }); + let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); + let (snapshot, edits) = self.fold_map.read(snapshot, edits); + let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); + let (snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + self.block_map.write(snapshot, edits); + } + pub fn insert_blocks( &mut self, blocks: impl IntoIterator>, @@ -395,6 +487,9 @@ impl DisplayMap { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self + .diff_map + .update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx)); let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); let (snapshot, edits) = self.fold_map.read(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); @@ -413,6 +508,9 @@ impl DisplayMap { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self + .diff_map + .update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx)); let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); let (snapshot, edits) = self.fold_map.read(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); @@ -431,6 +529,9 @@ impl DisplayMap { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self + .diff_map + .update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx)); let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); let (snapshot, edits) = self.fold_map.read(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); @@ -449,6 +550,9 @@ impl DisplayMap { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self + .diff_map + .update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx)); let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); let (snapshot, edits) = self.fold_map.read(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); @@ -524,7 +628,10 @@ impl DisplayMap { } let buffer_snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); - let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits); + let (snapshot, edits) = self.diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer_snapshot.clone(), edits, cx) + }); + let (snapshot, edits) = self.inlay_map.sync(snapshot.clone(), edits); let (snapshot, edits) = self.fold_map.read(snapshot, edits); let tab_size = Self::tab_size(&self.buffer, cx); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); @@ -710,17 +817,35 @@ impl DisplaySnapshot { self.fold_snapshot.fold_count() } + pub fn diff_hunks<'a>(&'a self) -> impl Iterator + 'a { + self.diff_snapshot() + .diff_hunks_in_range(0..self.buffer_snapshot.len()) + } + + pub fn diff_hunks_in_range<'a, T: ToOffset>( + &'a self, + range: Range, + ) -> impl Iterator + 'a { + self.diff_snapshot().diff_hunks_in_range(range) + } + + pub fn diff_hunks_in_range_rev<'a, T: ToOffset>( + &'a self, + range: Range, + ) -> impl Iterator + 'a { + self.diff_snapshot().diff_hunks_in_range_rev(range) + } + + pub fn has_diff_hunks(&self) -> bool { + self.diff_snapshot().has_diff_hunks() + } + pub fn is_empty(&self) -> bool { self.buffer_snapshot.len() == 0 } - pub fn buffer_rows( - &self, - start_row: DisplayRow, - ) -> impl Iterator> + '_ { - self.block_snapshot - .buffer_rows(BlockRow(start_row.0)) - .map(|row| row.map(MultiBufferRow)) + pub fn row_infos(&self, start_row: DisplayRow) -> impl Iterator + '_ { + self.block_snapshot.row_infos(BlockRow(start_row.0)) } pub fn widest_line_number(&self) -> u32 { @@ -729,7 +854,7 @@ impl DisplaySnapshot { pub fn prev_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) { loop { - let mut inlay_point = self.inlay_snapshot.to_inlay_point(point); + let mut inlay_point = self.inlay_snapshot.make_inlay_point(point); let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left); fold_point.0.column = 0; inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); @@ -751,7 +876,7 @@ impl DisplaySnapshot { ) -> (MultiBufferPoint, DisplayPoint) { let original_point = point; loop { - let mut inlay_point = self.inlay_snapshot.to_inlay_point(point); + let mut inlay_point = self.inlay_snapshot.make_inlay_point(point); let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right); fold_point.0.column = self.fold_snapshot.line_len(fold_point.row()); inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); @@ -795,7 +920,7 @@ impl DisplaySnapshot { } pub fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint { - let inlay_point = self.inlay_snapshot.to_inlay_point(point); + let inlay_point = self.inlay_snapshot.make_inlay_point(point); let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); let tab_point = self.tab_snapshot.to_tab_point(fold_point); let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); @@ -813,9 +938,15 @@ impl DisplaySnapshot { .to_offset(self.display_point_to_inlay_point(point, bias)) } + pub fn display_point_to_diff_offset(&self, point: DisplayPoint, bias: Bias) -> DiffOffset { + self.diff_snapshot() + .point_to_offset(self.display_point_to_diff_point(point, bias)) + } + pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset { - self.inlay_snapshot - .to_inlay_offset(anchor.to_offset(&self.buffer_snapshot)) + let multibuffer_offset = anchor.to_offset(&self.buffer_snapshot); + let diff_offset = self.diff_snapshot().to_diff_offset(multibuffer_offset); + self.inlay_snapshot.to_inlay_offset(diff_offset) } pub fn display_point_to_anchor(&self, point: DisplayPoint, bias: Bias) -> Anchor { @@ -831,6 +962,17 @@ impl DisplaySnapshot { fold_point.to_inlay_point(&self.fold_snapshot) } + fn diff_point_to_display_point(&self, diff_point: DiffPoint, bias: Bias) -> DisplayPoint { + let inlay_point = self.inlay_snapshot.to_inlay_point(diff_point); + let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); + self.fold_point_to_display_point(fold_point) + } + + fn display_point_to_diff_point(&self, point: DisplayPoint, bias: Bias) -> DiffPoint { + self.inlay_snapshot + .to_diff_point(self.display_point_to_inlay_point(point, bias)) + } + pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint { let block_point = point.0; let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias); @@ -1068,6 +1210,22 @@ impl DisplaySnapshot { }) } + pub fn anchor_before(&self, point: DisplayPoint) -> DisplayAnchor { + let diff_point = self.display_point_to_diff_point(point, Bias::Left); + self.diff_snapshot().point_to_anchor(diff_point, Bias::Left) + } + + pub fn anchor_after(&self, point: DisplayPoint) -> DisplayAnchor { + let diff_point = self.display_point_to_diff_point(point, Bias::Left); + self.diff_snapshot() + .point_to_anchor(diff_point, Bias::Right) + } + + pub fn anchor_to_point(&self, anchor: DisplayAnchor) -> DisplayPoint { + let diff_point = self.diff_snapshot().anchor_to_point(anchor); + self.diff_point_to_display_point(diff_point, Bias::Left) + } + pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint { let mut clipped = self.block_snapshot.clip_point(point.0, bias); if self.clip_at_line_ends { @@ -1307,6 +1465,51 @@ impl DisplaySnapshot { pub fn excerpt_header_height(&self) -> u32 { self.block_snapshot.excerpt_header_height } + + pub(crate) fn diff_snapshot(&self) -> &DiffMapSnapshot { + &self.inlay_snapshot.diff_map_snapshot + } +} + +#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)] +pub struct DisplayAnchor { + pub anchor: multi_buffer::Anchor, + pub diff_base_anchor: Option, +} + +impl DisplayAnchor { + pub fn min() -> Self { + Self { + anchor: multi_buffer::Anchor::min(), + diff_base_anchor: None, + } + } + + pub fn max() -> Self { + Self { + anchor: multi_buffer::Anchor::max(), + diff_base_anchor: None, + } + } + + pub fn cmp(&self, other: &DisplayAnchor, map: &DisplaySnapshot) -> std::cmp::Ordering { + map.diff_snapshot().compare_anchors(self, other) + } +} + +pub trait DisplayCoordinate { + fn to_display_anchor(self, map: &DisplaySnapshot) -> DisplayAnchor; +} + +impl DisplayCoordinate for DisplayAnchor { + fn to_display_anchor(self, _: &DisplaySnapshot) -> DisplayAnchor { + self + } +} +impl DisplayCoordinate for DisplayPoint { + fn to_display_anchor(self, map: &DisplaySnapshot) -> DisplayAnchor { + map.anchor_before(self) + } } #[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)] diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 00c87c84cb2dd..8648f6aab4029 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -2,7 +2,7 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, Highlights, }; -use crate::{EditorStyle, GutterDimensions}; +use crate::{EditorStyle, GutterDimensions, RowInfo}; use collections::{Bound, HashMap, HashSet}; use gpui::{AnyElement, AppContext, EntityId, Pixels, WindowContext}; use language::{Chunk, Patch, Point}; @@ -399,9 +399,9 @@ pub struct BlockChunks<'a> { } #[derive(Clone)] -pub struct BlockBufferRows<'a> { +pub struct BlockRows<'a> { transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, - input_buffer_rows: wrap_map::WrapBufferRows<'a>, + input_rows: wrap_map::WrapRows<'a>, output_row: BlockRow, started: bool, } @@ -1360,7 +1360,7 @@ impl BlockSnapshot { } } - pub(super) fn buffer_rows(&self, start_row: BlockRow) -> BlockBufferRows { + pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows { let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&start_row, Bias::Right, &()); let (output_start, input_start) = cursor.start(); @@ -1373,9 +1373,9 @@ impl BlockSnapshot { 0 }; let input_start_row = input_start.0 + overshoot; - BlockBufferRows { + BlockRows { transforms: cursor, - input_buffer_rows: self.wrap_snapshot.buffer_rows(input_start_row), + input_rows: self.wrap_snapshot.row_infos(input_start_row), output_row: start_row, started: false, } @@ -1766,8 +1766,8 @@ impl<'a> Iterator for BlockChunks<'a> { } } -impl<'a> Iterator for BlockBufferRows<'a> { - type Item = Option; +impl<'a> Iterator for BlockRows<'a> { + type Item = RowInfo; fn next(&mut self) -> Option { if self.started { @@ -1796,7 +1796,7 @@ impl<'a> Iterator for BlockBufferRows<'a> { .as_ref() .map_or(true, |block| block.is_replacement()) { - self.input_buffer_rows.seek(self.transforms.start().1 .0); + self.input_rows.seek(self.transforms.start().1 .0); } } @@ -1804,15 +1804,15 @@ impl<'a> Iterator for BlockBufferRows<'a> { if let Some(block) = transform.block.as_ref() { if block.is_replacement() && self.transforms.start().0 == self.output_row { if matches!(block, Block::FoldedBuffer { .. }) { - Some(None) + Some(RowInfo::default()) } else { - Some(self.input_buffer_rows.next().unwrap()) + Some(self.input_rows.next().unwrap()) } } else { - Some(None) + Some(RowInfo::default()) } } else { - Some(self.input_buffer_rows.next().unwrap()) + Some(self.input_rows.next().unwrap()) } } } @@ -1928,7 +1928,8 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) { mod tests { use super::*; use crate::display_map::{ - fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap, wrap_map::WrapMap, + diff_map::DiffMap, fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap, + wrap_map::WrapMap, }; use gpui::{div, font, px, AppContext, Context as _, Element}; use itertools::Itertools; @@ -1962,7 +1963,8 @@ mod tests { let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx)); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (diff_map, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx)); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap()); let (wrap_map, wraps_snapshot) = @@ -2087,7 +2089,10 @@ mod tests { ); assert_eq!( - snapshot.buffer_rows(BlockRow(0)).collect::>(), + snapshot + .row_infos(BlockRow(0)) + .map(|row_info| row_info.buffer_row) + .collect::>(), &[ Some(0), None, @@ -2108,8 +2113,10 @@ mod tests { buffer.snapshot(cx) }); - let (inlay_snapshot, inlay_edits) = - inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer_snapshot, subscription.consume().into_inner(), cx) + }); + let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits); let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap()); @@ -2171,8 +2178,8 @@ mod tests { .width; } - let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone()); + let (_, diff_snapshot) = DiffMap::new(multi_buffer.clone(), cx); + let (_, inlay_snapshot) = InlayMap::new(diff_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); @@ -2210,7 +2217,8 @@ mod tests { let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx)); let _subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); - let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_diff_map, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx)); + let (_inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap()); let (_wrap_map, wraps_snapshot) = @@ -2312,7 +2320,8 @@ mod tests { let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx)); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx)); + let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (_, wraps_snapshot) = cx.update(|cx| { @@ -2356,7 +2365,8 @@ mod tests { 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 (diff_map, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx)); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_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) = TabMap::new(fold_snapshot, tab_size); @@ -2383,10 +2393,14 @@ mod tests { 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 (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync( + buffer_snapshot, + buffer_subscription.consume().into_inner(), + cx, + ) + }); + let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot.clone(), diff_edits); 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| { @@ -2406,10 +2420,14 @@ mod tests { ); buffer.snapshot(cx) }); - let (inlay_snapshot, inlay_edits) = inlay_map.sync( - buffer_snapshot.clone(), - buffer_subscription.consume().into_inner(), - ); + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync( + buffer_snapshot.clone(), + buffer_subscription.consume().into_inner(), + cx, + ) + }); + let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits); 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| { @@ -2524,7 +2542,8 @@ mod tests { let buffer_id_2 = buffer_ids[1]; let buffer_id_3 = buffer_ids[2]; - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx)); + let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (_, wrap_snapshot) = @@ -2537,7 +2556,10 @@ mod tests { "\n\n\n111\n\n\n\n\n222\n\n\n333\n\n\n444\n\n\n\n\n555\n\n\n666\n" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2613,7 +2635,10 @@ mod tests { "\n\n\n111\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2688,7 +2713,10 @@ mod tests { "\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2753,7 +2781,10 @@ mod tests { ); assert_eq!(blocks_snapshot.text(), "\n\n\n\n\n\n\n\n555\n\n\n666\n\n"); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2807,7 +2838,10 @@ mod tests { "Should have extra newline for 111 buffer, due to a new block added when it was folded" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2861,7 +2895,10 @@ mod tests { "Should have a single, first buffer left after folding" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2895,7 +2932,8 @@ mod tests { assert_eq!(buffer_ids.len(), 1); let buffer_id = buffer_ids[0]; - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx)); + let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (_, wrap_snapshot) = @@ -2931,7 +2969,10 @@ mod tests { ); assert_eq!(blocks_snapshot.text(), "\n"); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![None, None], "When fully folded, should be no buffer rows" ); @@ -2977,7 +3018,8 @@ mod tests { }; let mut buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx)); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (diff_map, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx)); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (wrap_map, wraps_snapshot) = cx @@ -3035,8 +3077,10 @@ mod tests { }) .collect::>(); - let (inlay_snapshot, inlay_edits) = - inlay_map.sync(buffer_snapshot.clone(), vec![]); + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer_snapshot.clone(), vec![], cx) + }); + let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits); 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); @@ -3073,8 +3117,10 @@ mod tests { .map(|block| block.id) .collect::>(); - let (inlay_snapshot, inlay_edits) = - inlay_map.sync(buffer_snapshot.clone(), vec![]); + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer_snapshot.clone(), vec![], cx) + }); + let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits); 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); @@ -3094,8 +3140,11 @@ mod tests { log::info!("Noop fold/unfold operation on a singleton buffer"); continue; } + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer_snapshot.clone(), vec![], cx) + }); let (inlay_snapshot, inlay_edits) = - inlay_map.sync(buffer_snapshot.clone(), vec![]); + inlay_map.sync(diff_snapshot.clone(), diff_edits); 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); @@ -3180,8 +3229,10 @@ mod tests { } } - let (inlay_snapshot, inlay_edits) = - inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer_snapshot.clone(), buffer_edits, cx) + }); + let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits); 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| { @@ -3384,7 +3435,8 @@ mod tests { ); assert_eq!( blocks_snapshot - .buffer_rows(BlockRow(start_row as u32)) + .row_infos(BlockRow(start_row as u32)) + .map(|row_info| row_info.buffer_row) .collect::>(), &expected_buffer_rows[start_row..], "incorrect buffer_rows starting at row {:?}", diff --git a/crates/editor/src/display_map/diff_map.rs b/crates/editor/src/display_map/diff_map.rs new file mode 100644 index 0000000000000..0c4c32e67e0f2 --- /dev/null +++ b/crates/editor/src/display_map/diff_map.rs @@ -0,0 +1,2562 @@ +use super::{custom_highlights::CustomHighlightsChunks, DisplayAnchor}; +use crate::RowInfo; +use collections::HashMap; +use git::diff::DiffHunkStatus; +use gpui::{AppContext, Context as _, HighlightStyle, Model, ModelContext, Subscription}; +use language::{BufferChunks, BufferId, Chunk}; +use multi_buffer::{ + Anchor, AnchorRangeExt, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow, MultiBufferRows, + MultiBufferSnapshot, ToOffset, ToPoint as _, +}; +use project::buffer_store::BufferChangeSet; +use std::{ + any::TypeId, + mem::{self}, + ops::Range, + sync::Arc, +}; +use sum_tree::{Cursor, SumTree, TreeMap}; +use text::{Bias, Edit, Patch, Point, TextSummary, ToOffset as _, ToPoint as _}; +use util::debug_panic; + +pub(crate) struct DiffMap { + snapshot: DiffMapSnapshot, + multibuffer: Model, + diff_bases: HashMap, + all_hunks_expanded: bool, + edits_since_sync: Patch, +} + +struct ChangeSetState { + change_set: Model, + _subscription: Subscription, +} + +#[derive(Clone)] +struct DiffSnapshot { + diff: git::diff::BufferDiff, + base_text: language::BufferSnapshot, + base_text_version: usize, +} + +#[derive(Clone)] +pub struct DiffMapSnapshot { + diffs: TreeMap, + pub(crate) transforms: SumTree, + pub(crate) version: usize, + pub(crate) buffer: MultiBufferSnapshot, +} + +#[derive(Debug, Clone)] +pub(crate) enum DiffTransform { + BufferContent { + summary: TextSummary, + is_inserted_hunk: bool, + }, + DeletedHunk { + summary_including_newline: TextSummary, + buffer_id: BufferId, + base_text_byte_range: Range, + base_text_start: Point, + needs_newline: bool, + }, +} + +#[derive(Debug, Clone)] +pub(crate) struct DiffTransformSummary { + multibuffer_map: TextSummary, + diff_map: TextSummary, +} + +impl DiffTransformSummary { + pub fn multibuffer_point(&self) -> Point { + self.multibuffer_map.lines + } + pub fn multibuffer_offset(&self) -> usize { + self.multibuffer_map.len + } + pub fn diff_point(&self) -> DiffPoint { + DiffPoint(self.diff_map.lines) + } + pub fn diff_offset(&self) -> DiffOffset { + DiffOffset(self.diff_map.len) + } +} + +pub struct DiffMapChunks<'a> { + snapshot: &'a DiffMapSnapshot, + language_aware: bool, + cursor: Cursor<'a, DiffTransform, (DiffOffset, usize)>, + multibuffer_chunks: CustomHighlightsChunks<'a>, + multibuffer_chunk: Option>, + multibuffer_offset: usize, + offset: DiffOffset, + end_offset: DiffOffset, + diff_base_chunks: Option<(BufferId, BufferChunks<'a>)>, +} + +#[derive(Clone)] +pub struct DiffMapRows<'a> { + cursor: Cursor<'a, DiffTransform, (DiffPoint, Point)>, + diff_point: DiffPoint, + input_buffer_rows: MultiBufferRows<'a>, +} + +pub type DiffEdit = text::Edit; + +#[derive(Debug)] +enum ChangeKind { + DiffUpdated { base_changed: bool }, + InputEdited, + ExpandOrCollapseHunks { range: Range, expand: bool }, +} + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct DiffOffset(pub usize); + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct DiffPoint(pub Point); + +impl DiffPoint { + pub fn new(row: u32, col: u32) -> Self { + DiffPoint(Point::new(row, col)) + } + + pub fn row(&self) -> u32 { + self.0.row + } + pub fn column(&self) -> u32 { + self.0.column + } +} + +impl DiffMap { + pub fn new( + multibuffer: Model, + cx: &mut AppContext, + ) -> (Model, DiffMapSnapshot) { + let multibuffer_snapshot = multibuffer.read(cx).snapshot(cx); + let snapshot = DiffMapSnapshot { + diffs: TreeMap::default(), + version: 0, + transforms: SumTree::from_item( + DiffTransform::BufferContent { + summary: multibuffer_snapshot.text_summary(), + is_inserted_hunk: false, + }, + &(), + ), + buffer: multibuffer_snapshot, + }; + + let this = cx.new_model(|_| Self { + multibuffer, + snapshot: snapshot.clone(), + all_hunks_expanded: false, + diff_bases: HashMap::default(), + edits_since_sync: Patch::default(), + }); + + (this, snapshot) + } + + pub fn add_change_set( + &mut self, + change_set: Model, + cx: &mut ModelContext, + ) { + let buffer_id = change_set.read(cx).buffer_id; + self.buffer_diff_changed(change_set.clone(), cx); + self.diff_bases.insert( + buffer_id, + ChangeSetState { + _subscription: cx.observe(&change_set, Self::buffer_diff_changed), + change_set, + }, + ); + } + + pub fn diff_base_for(&self, buffer_id: BufferId) -> Option<&Model> { + self.diff_bases + .get(&buffer_id) + .map(|state| &state.change_set) + } + + pub fn sync( + &mut self, + multibuffer_snapshot: MultiBufferSnapshot, + buffer_edits: Vec>, + cx: &mut ModelContext, + ) -> (DiffMapSnapshot, Vec) { + let changes = buffer_edits + .iter() + .map(|edit| (edit.clone(), ChangeKind::InputEdited)) + .collect::>(); + + self.snapshot.buffer = multibuffer_snapshot.clone(); + self.recompute_transforms(changes, cx); + + ( + self.snapshot.clone(), + mem::take(&mut self.edits_since_sync).into_inner(), + ) + } + + fn buffer_diff_changed( + &mut self, + change_set: Model, + cx: &mut ModelContext, + ) { + let change_set = change_set.read(cx); + let buffer_id = change_set.buffer_id; + let diff = change_set.diff_to_buffer.clone(); + let base_text = change_set + .base_text + .as_ref() + .map(|buffer| buffer.read(cx).snapshot()); + let base_text_version_changed = self + .snapshot + .diffs + .get(&buffer_id) + .map_or(true, |snapshot| { + snapshot.base_text_version != change_set.base_text_version + }); + + if let Some(base_text) = base_text.clone() { + self.snapshot.diffs.insert( + buffer_id, + DiffSnapshot { + diff: diff.clone(), + base_text, + base_text_version: change_set.base_text_version, + }, + ); + } else { + self.snapshot.diffs.remove(&buffer_id); + } + + let multibuffer = self.multibuffer.read(cx); + let multibuffer_snapshot = self.snapshot.buffer(); + let changes = multibuffer + .ranges_for_buffer(buffer_id, cx) + .into_iter() + .map(|(_, range, _)| { + let multibuffer_start = multibuffer_snapshot.point_to_offset(range.start); + let multibuffer_end = multibuffer_snapshot.point_to_offset(range.end); + ( + text::Edit { + old: multibuffer_start..multibuffer_end, + new: multibuffer_start..multibuffer_end, + }, + ChangeKind::DiffUpdated { + base_changed: base_text_version_changed, + }, + ) + }) + .collect(); + self.recompute_transforms(changes, cx); + } + + pub(super) fn has_multiple_hunks(&self) -> bool { + self.snapshot + .diff_hunks_in_range(Anchor::min()..Anchor::max()) + .nth(1) + .is_some() + } + + pub(super) fn has_expanded_diff_hunks_in_ranges( + &self, + ranges: &[Range], + ) -> bool { + let mut cursor = self.snapshot.transforms.cursor::(&()); + let multibuffer_snapshot = self.snapshot.buffer(); + for range in ranges { + let range = range.to_point(multibuffer_snapshot); + let start = multibuffer_snapshot.point_to_offset(Point::new(range.start.row, 0)); + let end = multibuffer_snapshot.point_to_offset(Point::new(range.end.row + 1, 0)); + let start = start.saturating_sub(1); + let end = multibuffer_snapshot.len().min(end + 1); + cursor.seek(&start, Bias::Right, &()); + while *cursor.start() < end { + match cursor.item() { + Some(DiffTransform::DeletedHunk { .. }) + | Some(DiffTransform::BufferContent { + is_inserted_hunk: true, + .. + }) => return true, + _ => {} + } + cursor.next(&()); + } + } + false + } + + pub(super) fn expand_diff_hunks( + &mut self, + ranges: Vec>, + cx: &mut ModelContext, + ) { + self.expand_or_collapse_diff_hunks(ranges, true, cx); + } + + pub(super) fn collapse_diff_hunks( + &mut self, + ranges: Vec>, + cx: &mut ModelContext, + ) { + self.expand_or_collapse_diff_hunks(ranges, false, cx); + } + + pub(super) fn set_all_hunks_expanded(&mut self, cx: &mut ModelContext) { + self.all_hunks_expanded = true; + self.expand_or_collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], true, cx); + } + + fn expand_or_collapse_diff_hunks( + &mut self, + ranges: Vec>, + expand: bool, + cx: &mut ModelContext, + ) { + let multibuffer_snapshot = self.snapshot.buffer(); + let mut changes = Vec::new(); + for range in ranges.iter() { + let multibuffer_start = range.start.to_point(multibuffer_snapshot); + let multibuffer_end = range.end.to_point(multibuffer_snapshot); + let multibuffer_start = + multibuffer_snapshot.point_to_offset(Point::new(multibuffer_start.row, 0)); + let multibuffer_end = + multibuffer_snapshot.point_to_offset(Point::new(multibuffer_end.row + 1, 0)); + let expanded_start = multibuffer_start.saturating_sub(1); + let expanded_end = multibuffer_snapshot.len().min(multibuffer_end); + changes.push(( + text::Edit { + old: expanded_start..expanded_end, + new: expanded_start..expanded_end, + }, + ChangeKind::ExpandOrCollapseHunks { + range: multibuffer_start..multibuffer_end, + expand, + }, + )); + } + + self.recompute_transforms(changes, cx); + } + + fn recompute_transforms( + &mut self, + changes: Vec<(text::Edit, ChangeKind)>, + cx: &mut ModelContext, + ) { + let multibuffer = self.multibuffer.read(cx); + let multibuffer_snapshot = self.snapshot.buffer(); + + let mut cursor = self.snapshot.transforms.cursor::<(usize, DiffOffset)>(&()); + let mut new_transforms = SumTree::default(); + let mut edits = Patch::default(); + + let mut changes = changes.into_iter().peekable(); + let mut delta = 0_isize; + while let Some((mut edit, mut operation)) = changes.next() { + let mut to_skip = cursor.slice(&edit.old.start, Bias::Left, &()); + while cursor.end(&()).0 < edit.old.start + || (cursor.end(&()).0 == edit.old.start && cursor.start().0 < edit.old.start) + { + to_skip.extend(cursor.item().cloned(), &()); + cursor.next(&()); + } + + self.append_transforms(&mut new_transforms, to_skip); + + let mut end_of_current_insert = 0; + loop { + let multibuffer_range = edit.new.clone(); + let edit_old_start = + cursor.start().1 + DiffOffset(edit.old.start - cursor.start().0); + let mut edit_old_end = + cursor.start().1 + DiffOffset(edit.old.end - cursor.start().0); + + for (buffer, buffer_range, excerpt_id) in + multibuffer.range_to_buffer_ranges(multibuffer_range.clone(), cx) + { + let excerpt_range = multibuffer_snapshot + .range_for_excerpt::(excerpt_id) + .unwrap(); + let excerpt_buffer_range = multibuffer_snapshot + .buffer_range_for_excerpt(excerpt_id) + .unwrap(); + let buffer_id = buffer.read(cx).remote_id(); + let diff_state = self.snapshot.diffs.get(&buffer_id); + + let buffer = buffer.read(cx); + let buffer_anchor_range = buffer.anchor_before(buffer_range.start) + ..buffer.anchor_after(buffer_range.end); + let change_start_buffer_offset = buffer_range.start; + if let Some(diff_state) = diff_state { + let diff = &diff_state.diff; + let base_text = &diff_state.base_text; + + for hunk in diff.hunks_intersecting_range(buffer_anchor_range, buffer) { + let hunk_start_buffer_offset = + hunk.buffer_range.start.to_offset(buffer); + let hunk_end_buffer_offset = hunk.buffer_range.end.to_offset(buffer); + + let excerpt_buffer_range_start_offset = + excerpt_buffer_range.start.to_offset(buffer); + let hunk_start_multibuffer_offset = excerpt_range.start + + hunk_start_buffer_offset + .saturating_sub(excerpt_buffer_range_start_offset); + + self.push_buffer_content_transform( + &mut new_transforms, + hunk_start_multibuffer_offset, + end_of_current_insert, + ); + + while cursor.end(&()).0 < hunk_start_multibuffer_offset + || (cursor.end(&()).0 == hunk_start_multibuffer_offset + && cursor.start().0 < hunk_start_multibuffer_offset) + { + let Some(item) = cursor.item() else { + break; + }; + if let DiffTransform::DeletedHunk { .. } = item { + let old_range = cursor.start().1..cursor.end(&()).1; + let new_offset = + DiffOffset((old_range.start.0 as isize + delta) as usize); + delta -= (old_range.end - old_range.start).0 as isize; + let edit = Edit { + old: old_range, + new: new_offset..new_offset, + }; + edits.push(edit); + } + cursor.next(&()); + } + + let mut was_previously_expanded = false; + let mut previous_expanded_summary = TextSummary::default(); + if cursor.start().0 == hunk_start_multibuffer_offset { + match cursor.item() { + Some(DiffTransform::DeletedHunk { + summary_including_newline, + .. + }) => { + was_previously_expanded = true; + previous_expanded_summary = + summary_including_newline.clone(); + } + Some(DiffTransform::BufferContent { + is_inserted_hunk, .. + }) => { + was_previously_expanded = *is_inserted_hunk; + } + None => {} + }; + } + + let hunk_is_deletion = + hunk_start_buffer_offset == hunk_end_buffer_offset; + + let should_expand_hunk = match &operation { + ChangeKind::DiffUpdated { base_changed: true } => { + self.all_hunks_expanded + } + ChangeKind::ExpandOrCollapseHunks { range, expand } => { + let intersects = hunk_is_deletion + || (hunk_start_buffer_offset < range.end + && hunk_end_buffer_offset > range.start); + if *expand { + was_previously_expanded + || self.all_hunks_expanded + || intersects + } else { + !intersects + && (was_previously_expanded || self.all_hunks_expanded) + } + } + _ => was_previously_expanded || self.all_hunks_expanded, + }; + + if should_expand_hunk { + if hunk.diff_base_byte_range.len() > 0 + && hunk_start_buffer_offset >= change_start_buffer_offset + { + let mut text_cursor = base_text.as_rope().cursor(0); + let base_text_start = text_cursor + .summary::(hunk.diff_base_byte_range.start); + let mut base_text_summary = text_cursor + .summary::(hunk.diff_base_byte_range.end); + let mut needs_newline = false; + let mut diff_byte_range = + DiffOffset(hunk.diff_base_byte_range.len()); + if base_text_summary.last_line_chars > 0 { + diff_byte_range.0 += 1; + base_text_summary.add_newline(); + needs_newline = true; + } + + if !was_previously_expanded + || base_text_summary != previous_expanded_summary + { + let hunk_overshoot = + hunk_start_multibuffer_offset - cursor.start().0; + let old_start = + cursor.start().1 + DiffOffset(hunk_overshoot); + let old_end = + old_start + DiffOffset(previous_expanded_summary.len); + let new_start = + DiffOffset(new_transforms.summary().diff_map.len); + let new_end = new_start + diff_byte_range; + delta += diff_byte_range.0 as isize; + let edit = Edit { + old: old_start..old_end, + new: new_start..new_end, + }; + edits.push(edit); + } + + new_transforms.push( + DiffTransform::DeletedHunk { + base_text_byte_range: hunk.diff_base_byte_range.clone(), + summary_including_newline: base_text_summary, + buffer_id, + base_text_start, + needs_newline, + }, + &(), + ); + } + + if hunk_end_buffer_offset > hunk_start_buffer_offset { + let hunk_end_multibuffer_offset = excerpt_range.start + + hunk_end_buffer_offset + - excerpt_buffer_range_start_offset; + end_of_current_insert = hunk_end_multibuffer_offset; + } + + if was_previously_expanded { + cursor.next(&()); + } + } + } + } + } + + while cursor.end(&()).0 <= edit.old.end { + let Some(item) = cursor.item() else { + break; + }; + if let DiffTransform::DeletedHunk { .. } = item { + let old_range = cursor.start().1..cursor.end(&()).1; + let new_offset = DiffOffset((old_range.start.0 as isize + delta) as usize); + delta -= (old_range.end - old_range.start).0 as isize; + let edit = Edit { + old: old_range, + new: new_offset..new_offset, + }; + edits.push(edit); + } + + edit_old_end = cursor.start().1 + DiffOffset(edit.old.end - cursor.start().0); + + cursor.next(&()); + } + + self.push_buffer_content_transform( + &mut new_transforms, + edit.new.end, + end_of_current_insert, + ); + + if let ChangeKind::InputEdited = operation { + let edit_new_start = DiffOffset((edit_old_start.0 as isize + delta) as usize); + delta += (edit.new.end - edit.new.start) as isize + - (edit.old.end - edit.old.start) as isize; + let edit_new_end = DiffOffset((edit_old_end.0 as isize + delta) as usize); + let edit = DiffEdit { + old: edit_old_start..edit_old_end, + new: edit_new_start..edit_new_end, + }; + edits.push(edit); + } + + if let Some((next_edit, _)) = changes.peek() { + if next_edit.old.start < cursor.end(&()).0 { + (edit, operation) = changes.next().unwrap(); + continue; + } + } + + let suffix = cursor.end(&()).0 - edit.old.end; + let transform_end = new_transforms.summary().multibuffer_map.len + suffix; + self.push_buffer_content_transform( + &mut new_transforms, + transform_end, + end_of_current_insert, + ); + cursor.next(&()); + break; + } + } + + self.append_transforms(&mut new_transforms, cursor.suffix(&())); + self.edits_since_sync = self.edits_since_sync.compose(edits); + + drop(cursor); + self.snapshot.transforms = new_transforms; + self.snapshot.version += 1; + cx.notify(); + + #[cfg(test)] + self.check_invariants(); + } + + fn append_transforms( + &self, + new_transforms: &mut SumTree, + subtree: SumTree, + ) { + if let Some(DiffTransform::BufferContent { + is_inserted_hunk, + summary, + }) = subtree.first() + { + if self.extend_last_buffer_content_transform( + new_transforms, + *is_inserted_hunk, + summary.clone(), + ) { + let mut cursor = subtree.cursor::<()>(&()); + cursor.next(&()); + cursor.next(&()); + new_transforms.append(cursor.suffix(&()), &()); + return; + } + } + new_transforms.append(subtree, &()); + } + + fn push_buffer_content_transform( + &self, + new_transforms: &mut SumTree, + end_offset: usize, + end_of_current_inserted_hunk: usize, + ) { + for (end_offset, region_is_inserted_hunk) in [ + (end_offset.min(end_of_current_inserted_hunk), true), + (end_offset, false), + ] { + let start_offset = new_transforms.summary().multibuffer_map.len; + if end_offset <= start_offset { + continue; + } + let summary_to_add = self + .snapshot + .buffer + .text_summary_for_range::(start_offset..end_offset); + + if !self.extend_last_buffer_content_transform( + new_transforms, + region_is_inserted_hunk, + summary_to_add.clone(), + ) { + new_transforms.push( + DiffTransform::BufferContent { + summary: summary_to_add, + is_inserted_hunk: region_is_inserted_hunk, + }, + &(), + ) + } + } + } + + fn extend_last_buffer_content_transform( + &self, + new_transforms: &mut SumTree, + region_is_inserted_hunk: bool, + summary_to_add: TextSummary, + ) -> bool { + let mut did_extend = false; + new_transforms.update_last( + |last_transform| { + if let DiffTransform::BufferContent { + summary, + is_inserted_hunk, + } = last_transform + { + if *is_inserted_hunk == region_is_inserted_hunk { + *summary += summary_to_add.clone(); + did_extend = true; + } + } + }, + &(), + ); + did_extend + } + + #[cfg(test)] + fn check_invariants(&self) { + let snapshot = &self.snapshot; + if snapshot.transforms.summary().multibuffer_map.len != snapshot.buffer.len() { + panic!( + "incorrect input length. expected {}, got {}. transforms: {:+?}", + snapshot.buffer.len(), + snapshot.transforms.summary().multibuffer_map.len, + snapshot.transforms.items(&()), + ); + } + + let mut prev_transform: Option<&DiffTransform> = None; + for item in snapshot.transforms.iter() { + if let DiffTransform::BufferContent { + summary, + is_inserted_hunk, + } = item + { + if let Some(DiffTransform::BufferContent { + is_inserted_hunk: prev_is_inserted_hunk, + .. + }) = prev_transform + { + if *is_inserted_hunk == *prev_is_inserted_hunk { + panic!( + "multiple adjacent buffer content transforms with is_inserted_hunk = {is_inserted_hunk}. transforms: {:+?}", + snapshot.transforms.items(&())); + } + } + if summary.len == 0 && !snapshot.buffer().is_empty() { + panic!("empty buffer content transform"); + } + } + prev_transform = Some(item); + } + } +} + +impl DiffMapSnapshot { + pub fn diff_hunks_in_range<'a, T: ToOffset>( + &'a self, + range: Range, + ) -> impl Iterator + 'a { + let buffer_snapshot = self.buffer(); + let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot); + buffer_snapshot + .excerpts_for_range(range.clone()) + .filter_map(move |excerpt| { + let buffer = excerpt.buffer(); + let buffer_id = buffer.remote_id(); + let diff = &self.diffs.get(&buffer_id)?.diff; + let buffer_range = excerpt.map_range_to_buffer(range.clone()); + let buffer_range = + buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end); + Some( + diff.hunks_intersecting_range(buffer_range, excerpt.buffer()) + .map(move |hunk| { + let start = + excerpt.map_point_from_buffer(Point::new(hunk.row_range.start, 0)); + let end = + excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0)); + MultiBufferDiffHunk { + row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row), + buffer_id, + buffer_range: hunk.buffer_range.clone(), + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + } + }), + ) + }) + .flatten() + } + + pub fn diff_hunks_in_range_rev<'a, T: ToOffset>( + &'a self, + range: Range, + ) -> impl Iterator + 'a { + let buffer_snapshot = self.buffer(); + let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot); + buffer_snapshot + .excerpts_for_range_rev(range.clone()) + .filter_map(move |excerpt| { + let buffer = excerpt.buffer(); + let buffer_id = buffer.remote_id(); + let diff = &self.diffs.get(&buffer_id)?.diff; + let buffer_range = excerpt.map_range_to_buffer(range.clone()); + let buffer_range = + buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end); + Some( + diff.hunks_intersecting_range_rev(buffer_range, excerpt.buffer()) + .map(move |hunk| { + let start_row = excerpt + .map_point_from_buffer(Point::new(hunk.row_range.start, 0)) + .row; + let end_row = excerpt + .map_point_from_buffer(Point::new(hunk.row_range.end, 0)) + .row; + MultiBufferDiffHunk { + row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row), + buffer_id, + buffer_range: hunk.buffer_range.clone(), + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + } + }), + ) + }) + .flatten() + } + + pub fn has_diff_hunks(&self) -> bool { + self.diffs.values().any(|diff| !diff.diff.is_empty()) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn text(&self) -> String { + self.chunks(DiffOffset(0)..self.len(), false, None) + .map(|c| c.text) + .collect() + } + + pub fn len(&self) -> DiffOffset { + DiffOffset(self.transforms.summary().diff_map.len) + } + + pub fn max_point(&self) -> DiffPoint { + DiffPoint(self.transforms.summary().diff_map.lines) + } + + pub fn text_summary(&self) -> TextSummary { + self.transforms.summary().diff_map.clone() + } + + pub fn text_summary_for_range(&self, range: Range) -> TextSummary { + let mut cursor = self.transforms.cursor::<(DiffOffset, usize)>(&()); + cursor.seek(&range.start, Bias::Right, &()); + + let Some(first_transform) = cursor.item() else { + return TextSummary::default(); + }; + + let (diff_transform_start, multibuffer_transform_start) = cursor.start().clone(); + let (diff_transform_end, _) = cursor.end(&()); + let diff_start = range.start; + let diff_end = std::cmp::min(range.end, diff_transform_end); + + let mut result = match first_transform { + DiffTransform::BufferContent { .. } => { + let multibuffer_start = + multibuffer_transform_start + (diff_start - diff_transform_start).0; + let multibuffer_end = + multibuffer_transform_start + (diff_end - diff_transform_start).0; + + self.buffer + .text_summary_for_range(multibuffer_start..multibuffer_end) + } + DiffTransform::DeletedHunk { + buffer_id, + base_text_byte_range, + needs_newline, + .. + } => { + let buffer_start = + base_text_byte_range.start + (diff_start - diff_transform_start).0; + let mut buffer_end = + base_text_byte_range.start + (diff_end - diff_transform_start).0; + let Some(buffer_diff) = self.diffs.get(buffer_id) else { + panic!("{:?} is in non-extant deleted hunk", range.start) + }; + + if *needs_newline && diff_end == diff_transform_end { + buffer_end -= 1; + } + + let mut summary = buffer_diff + .base_text + .text_summary_for_range::(buffer_start..buffer_end); + + if *needs_newline && diff_end == diff_transform_end { + summary.add_newline(); + } + + summary + } + }; + if range.end < diff_transform_end { + return result; + } + + cursor.next(&()); + result = result + cursor.summary(&range.end, Bias::Right, &()); + + let Some(last_transform) = cursor.item() else { + return result; + }; + + let (diff_transform_start, multibuffer_transform_start) = cursor.start().clone(); + + result += match last_transform { + DiffTransform::BufferContent { .. } => { + let multibuffer_end = + multibuffer_transform_start + (range.end - diff_transform_start).0; + + self.buffer.text_summary_for_range::( + multibuffer_transform_start..multibuffer_end, + ) + } + DiffTransform::DeletedHunk { + base_text_byte_range, + buffer_id, + needs_newline, + .. + } => { + let buffer_end = base_text_byte_range.start + (range.end - diff_transform_start).0; + let Some(buffer_diff) = self.diffs.get(buffer_id) else { + panic!("{:?} is in non-extant deleted hunk", range.end) + }; + + let mut result = buffer_diff + .base_text + .text_summary_for_range::( + base_text_byte_range.start..buffer_end, + ); + + if *needs_newline && buffer_end == base_text_byte_range.end + 1 { + result.add_newline(); + } + + result + } + }; + + result + } + + pub fn buffer(&self) -> &MultiBufferSnapshot { + &self.buffer + } + + pub fn offset_to_point(&self, offset: DiffOffset) -> DiffPoint { + let mut cursor = self.transforms.cursor::(&()); + cursor.seek(&offset, Bias::Right, &()); + let start_transform = cursor.start(); + let overshoot = offset - start_transform.diff_offset(); + if overshoot.0 == 0 { + return start_transform.diff_point(); + } + + match cursor.item() { + Some(DiffTransform::BufferContent { .. }) => { + let multibuffer_offset = start_transform.multibuffer_offset() + overshoot.0; + let multibuffer_point = self.buffer.offset_to_point(multibuffer_offset); + start_transform.diff_point() + + DiffPoint(multibuffer_point - start_transform.multibuffer_point()) + } + Some(DiffTransform::DeletedHunk { + buffer_id, + base_text_start, + base_text_byte_range, + .. + }) => { + let Some(buffer_diff) = self.diffs.get(buffer_id) else { + panic!("{:?} is in non-extant deleted hunk", offset) + }; + let buffer_offset = base_text_byte_range.start + overshoot.0; + let buffer_point = buffer_diff.base_text.offset_to_point(buffer_offset); + start_transform.diff_point() + DiffPoint(buffer_point - base_text_start) + } + None => { + panic!("{:?} is past end of buffer", offset) + } + } + } + + pub fn clip_point(&self, point: DiffPoint, bias: Bias) -> DiffPoint { + let mut cursor = self.transforms.cursor::(&()); + cursor.seek(&point, Bias::Right, &()); + let start_transform = cursor.start(); + let overshoot = point - start_transform.diff_point(); + if overshoot.0.is_zero() { + return start_transform.diff_point(); + } + + match cursor.item() { + Some(DiffTransform::BufferContent { .. }) => { + let inlay_point = start_transform.multibuffer_point() + overshoot.0; + let clipped = self.buffer.clip_point(inlay_point, bias); + start_transform.diff_point() + + DiffPoint(clipped - start_transform.multibuffer_point()) + } + Some(DiffTransform::DeletedHunk { + buffer_id, + base_text_start, + .. + }) => { + let Some(buffer_diff) = self.diffs.get(buffer_id) else { + panic!("{:?} is in non-extant deleted hunk", point) + }; + let buffer_point = *base_text_start + overshoot.0; + let clipped = buffer_diff.base_text.clip_point(buffer_point, bias); + start_transform.diff_point() + DiffPoint(clipped - base_text_start) + } + None => cursor.end(&()).diff_point(), + } + } + + pub fn point_to_offset(&self, point: DiffPoint) -> DiffOffset { + let mut cursor = self.transforms.cursor::(&()); + cursor.seek(&point, Bias::Right, &()); + let start_transform = cursor.start(); + let overshoot = point - start_transform.diff_point(); + if overshoot.0.is_zero() { + return start_transform.diff_offset(); + } + + match cursor.item() { + Some(DiffTransform::BufferContent { .. }) => { + let multibuffer_point = start_transform.multibuffer_point() + overshoot.0; + let multibuffer_offset = self.buffer.point_to_offset(multibuffer_point); + start_transform.diff_offset() + + DiffOffset(multibuffer_offset - start_transform.multibuffer_offset()) + } + Some(DiffTransform::DeletedHunk { + buffer_id, + base_text_start, + base_text_byte_range, + .. + }) => { + let Some(buffer_diff) = self.diffs.get(buffer_id) else { + panic!("{:?} is in non-extant deleted hunk", point) + }; + let buffer_point = *base_text_start + overshoot.0; + let buffer_offset = buffer_diff.base_text.point_to_offset(buffer_point); + start_transform.diff_offset() + + DiffOffset(buffer_offset - base_text_byte_range.start) + } + None => { + panic!("{:?} is past end of buffer", point) + } + } + } + + pub fn point_to_anchor(&self, point: DiffPoint, bias: Bias) -> DisplayAnchor { + let mut cursor = self.transforms.cursor::<(DiffPoint, Point)>(&()); + cursor.seek(&point, Bias::Right, &()); + let (diff_start, multibuffer_start) = *cursor.start(); + match cursor.item() { + Some(DiffTransform::BufferContent { .. }) => { + let multibuffer_point = multibuffer_start + (point - diff_start).0; + DisplayAnchor { + anchor: self.buffer.anchor_at(multibuffer_point, bias), + diff_base_anchor: None, + } + } + Some(DiffTransform::DeletedHunk { + buffer_id, + base_text_start, + .. + }) => { + let diff_base_point = *base_text_start + (point - diff_start).0; + let diff_base_anchor = if let Some(diff_base_snapshot) = self.diffs.get(&buffer_id) + { + Some( + diff_base_snapshot + .base_text + .anchor_at(diff_base_point, bias), + ) + } else { + debug_panic!("{} is missing diff base", buffer_id); + None + }; + + DisplayAnchor { + anchor: self.buffer.anchor_at(multibuffer_start, Bias::Left), + diff_base_anchor, + } + } + None => { + panic!("{:?} is out of range", point) + } + } + } + + pub fn anchor_to_point(&self, anchor: DisplayAnchor) -> DiffPoint { + let multibuffer_point = anchor.anchor.to_point(&self.buffer); + + let mut cursor = self.transforms.cursor::<(Point, DiffPoint)>(&()); + cursor.seek(&multibuffer_point, Bias::Left, &()); + + if let Some(DiffTransform::DeletedHunk { + buffer_id, + base_text_start, + .. + }) = cursor.item() + { + if let Some(diff_base_anchor) = anchor.diff_base_anchor { + if let Some(diff_base_snapshot) = self.diffs.get(&buffer_id) { + if diff_base_anchor.buffer_id == Some(diff_base_snapshot.base_text.remote_id()) + { + let (_, diff_start) = *cursor.start(); + let base_text_point = + diff_base_anchor.to_point(&diff_base_snapshot.base_text); + return diff_start + DiffPoint(base_text_point - base_text_start); + } + } + } else { + cursor.next(&()) + } + } + + debug_assert!(matches!( + cursor.item(), + Some(DiffTransform::BufferContent { .. }) + )); + + let (multibuffer_start, diff_start) = *cursor.start(); + diff_start + DiffPoint(multibuffer_point - multibuffer_start) + } + + pub fn compare_anchors(&self, a: &DisplayAnchor, b: &DisplayAnchor) -> std::cmp::Ordering { + let buffer_cmp = a.anchor.cmp(&b.anchor, self.buffer()); + if buffer_cmp != std::cmp::Ordering::Equal { + return buffer_cmp; + } + + match (a.diff_base_anchor.is_some(), b.diff_base_anchor.is_some()) { + (false, true) => return std::cmp::Ordering::Greater, + (true, false) => return std::cmp::Ordering::Less, + (false, false) => return std::cmp::Ordering::Equal, + (true, true) => {} + } + + let diff_anchor_a = a.diff_base_anchor.unwrap(); + let diff_anchor_b = b.diff_base_anchor.unwrap(); + + if diff_anchor_a.buffer_id != diff_anchor_b.buffer_id || diff_anchor_a.buffer_id.is_none() { + return std::cmp::Ordering::Equal; + } + + let Some(diff_base_snapshot) = self.diffs.get(&diff_anchor_a.buffer_id.unwrap()) else { + return std::cmp::Ordering::Equal; + }; + + diff_anchor_a.cmp(&diff_anchor_b, &diff_base_snapshot.base_text) + } + + pub fn to_multibuffer_offset(&self, offset: DiffOffset) -> usize { + let mut cursor = self.transforms.cursor::<(DiffOffset, usize)>(&()); + cursor.seek(&offset, Bias::Right, &()); + let mut multibuffer_offset = cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = cursor.item() { + let overshoot = offset.0 - cursor.start().0 .0; + multibuffer_offset += overshoot; + } + multibuffer_offset + } + + pub fn to_multibuffer_point(&self, point: DiffPoint) -> Point { + let mut cursor = self.transforms.cursor::<(DiffPoint, Point)>(&()); + cursor.seek(&point, Bias::Right, &()); + let mut inlay_point = cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = cursor.item() { + let overshoot = point.0 - cursor.start().0 .0; + inlay_point += overshoot; + } + inlay_point + } + + pub fn to_diff_offset(&self, multibuffer_offset: usize) -> DiffOffset { + let mut cursor = self.transforms.cursor::<(usize, DiffOffset)>(&()); + cursor.seek(&multibuffer_offset, Bias::Right, &()); + let mut diff_offset = cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = cursor.item() { + let overshoot = multibuffer_offset - cursor.start().0; + diff_offset.0 += overshoot; + } + diff_offset + } + + pub fn to_diff_point(&self, multibuffer_point: Point) -> DiffPoint { + let mut cursor = self.transforms.cursor::<(Point, DiffPoint)>(&()); + cursor.seek(&multibuffer_point, Bias::Right, &()); + let mut diff_point = cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = cursor.item() { + let overshoot = multibuffer_point - cursor.start().0; + diff_point.0 += overshoot; + } + diff_point + } + + pub(crate) fn chunks<'a>( + &'a self, + range: Range, + language_aware: bool, + text_highlights: Option<&'a TreeMap>)>>>, + ) -> DiffMapChunks<'a> { + let mut cursor = self.transforms.cursor::<(DiffOffset, usize)>(&()); + + cursor.seek(&range.end, Bias::Right, &()); + let mut multibuffer_end = cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = cursor.item() { + let overshoot = range.end.0 - cursor.start().0 .0; + multibuffer_end += overshoot; + } + + cursor.seek(&range.start, Bias::Right, &()); + let mut multibuffer_start = cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = cursor.item() { + let overshoot = range.start.0 - cursor.start().0 .0; + multibuffer_start += overshoot; + } + + let multibuffer_chunks = CustomHighlightsChunks::new( + multibuffer_start..multibuffer_end, + language_aware, + text_highlights, + &self.buffer, + ); + + DiffMapChunks { + snapshot: self, + language_aware, + cursor, + multibuffer_chunk: None, + multibuffer_chunks, + multibuffer_offset: multibuffer_start, + offset: range.start, + diff_base_chunks: None, + end_offset: range.end, + } + } + + pub fn row_infos(&self, start_row: u32) -> DiffMapRows { + if start_row > self.transforms.summary().diff_map.lines.row { + panic!("invalid diff map row {}", start_row); + } + + let diff_point = DiffPoint(Point::new(start_row, 0)); + let mut cursor = self.transforms.cursor::<(DiffPoint, Point)>(&()); + cursor.seek(&diff_point, Bias::Right, &()); + + let (diff_transform_start, buffer_transform_start) = cursor.start().clone(); + + let overshoot = if matches!(cursor.item(), Some(DiffTransform::BufferContent { .. })) { + diff_point.row() - diff_transform_start.row() + } else { + 0 + }; + let input_buffer_rows = self + .buffer + .buffer_rows(MultiBufferRow(buffer_transform_start.row + overshoot)); + + DiffMapRows { + diff_point, + input_buffer_rows, + cursor, + } + } +} + +impl<'a> DiffMapChunks<'a> { + pub fn seek(&mut self, range: Range) { + self.cursor.seek(&range.end, Bias::Right, &()); + let mut inlay_end = self.cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = self.cursor.item() { + let overshoot = range.end.0 - self.cursor.start().0 .0; + inlay_end += overshoot; + } + + self.cursor.seek(&range.start, Bias::Right, &()); + let mut inlay_start = self.cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = self.cursor.item() { + let overshoot = range.start.0 - self.cursor.start().0 .0; + inlay_start += overshoot; + } + + self.multibuffer_chunks.seek(inlay_start..inlay_end); + self.multibuffer_chunk.take(); + self.multibuffer_offset = inlay_start; + self.offset = range.start; + self.end_offset = range.end; + } +} + +impl<'a> Iterator for DiffMapChunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + if self.offset >= self.end_offset { + return None; + } + if self.offset == self.cursor.end(&()).0 { + self.cursor.next(&()); + } + + let transform = self.cursor.item()?; + + match transform { + DiffTransform::BufferContent { summary, .. } => { + let chunk = self + .multibuffer_chunk + .get_or_insert_with(|| self.multibuffer_chunks.next().unwrap()); + + let chunk_end = self.offset + DiffOffset(chunk.text.len()); + let mut transform_end = self.cursor.start().0 + DiffOffset(summary.len); + + if transform_end > self.end_offset { + transform_end = self.end_offset + } + + if transform_end < chunk_end { + let (before, after) = chunk.text.split_at(transform_end.0 - self.offset.0); + self.offset = transform_end; + chunk.text = after; + Some(Chunk { + text: before, + ..chunk.clone() + }) + } else { + self.offset = chunk_end; + self.multibuffer_chunk.take() + } + } + DiffTransform::DeletedHunk { + buffer_id, + base_text_byte_range, + needs_newline, + .. + } => { + let hunk_start_offset = self.cursor.start().0; + let base_text_start_offset = base_text_byte_range.start; + let base_text_end_offset = base_text_byte_range.end; + let diff_base_end_offset = base_text_end_offset + .min(base_text_start_offset + self.end_offset.0 - hunk_start_offset.0); + let diff_base_start_offset = + base_text_start_offset + self.offset.0 - hunk_start_offset.0; + + let mut chunks = if let Some((_, mut chunks)) = self + .diff_base_chunks + .take() + .filter(|(id, _)| id == buffer_id) + { + if chunks.offset() != diff_base_start_offset { + chunks.seek(diff_base_start_offset..diff_base_end_offset); + } + chunks + } else { + let base_buffer = &self.snapshot.diffs.get(&buffer_id)?.base_text; + base_buffer.chunks( + diff_base_start_offset..diff_base_end_offset, + self.language_aware, + ) + }; + + let chunk = if let Some(chunk) = chunks.next() { + self.offset.0 += chunk.text.len(); + self.diff_base_chunks = Some((*buffer_id, chunks)); + chunk + } else { + debug_assert!(needs_newline); + self.offset.0 += "\n".len(); + Chunk { + text: "\n", + ..Default::default() + } + }; + Some(chunk) + } + } + } +} + +impl<'a> DiffMapRows<'a> { + pub fn seek(&mut self, row: u32) { + self.diff_point = DiffPoint::new(row, 0); + self.cursor.seek(&self.diff_point, Bias::Right, &()); + let (diff_transform_start, inlay_transform_start) = self.cursor.start().clone(); + let overshoot = if matches!( + self.cursor.item(), + Some(DiffTransform::BufferContent { .. }) + ) { + self.diff_point.row() - diff_transform_start.row() + } else { + 0 + }; + self.input_buffer_rows + .seek(MultiBufferRow(inlay_transform_start.row + overshoot)); + } +} + +impl<'a> Iterator for DiffMapRows<'a> { + type Item = RowInfo; + + fn next(&mut self) -> Option { + let result = match self.cursor.item() { + Some(DiffTransform::DeletedHunk { .. }) => Some(RowInfo { + buffer_row: None, + diff_status: Some(DiffHunkStatus::Removed), + }), + Some(DiffTransform::BufferContent { + is_inserted_hunk, .. + }) => { + let row = self.input_buffer_rows.next(); + row.map(|row| RowInfo { + buffer_row: row, + diff_status: if *is_inserted_hunk { + Some(DiffHunkStatus::Added) + } else { + None + }, + }) + } + None => self.input_buffer_rows.next().map(|row| RowInfo { + buffer_row: row, + diff_status: None, + }), + }; + self.diff_point.0 += Point::new(1, 0); + if self.diff_point >= self.cursor.end(&()).0 { + self.cursor.next(&()); + } + result + } +} + +impl sum_tree::Item for DiffTransform { + type Summary = DiffTransformSummary; + + fn summary(&self, _: &::Context) -> Self::Summary { + match self { + DiffTransform::BufferContent { summary, .. } => DiffTransformSummary { + multibuffer_map: summary.clone(), + diff_map: summary.clone(), + }, + DiffTransform::DeletedHunk { + summary_including_newline: summary, + .. + } => DiffTransformSummary { + multibuffer_map: TextSummary::default(), + diff_map: summary.clone(), + }, + } + } +} + +impl sum_tree::Summary for DiffTransformSummary { + type Context = (); + + fn zero(_: &Self::Context) -> Self { + DiffTransformSummary { + multibuffer_map: TextSummary::default(), + diff_map: TextSummary::default(), + } + } + + fn add_summary(&mut self, summary: &Self, _: &Self::Context) { + self.multibuffer_map += &summary.multibuffer_map; + self.diff_map += &summary.diff_map; + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for usize { + fn zero(_: &()) -> Self { + 0 + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + *self += summary.multibuffer_map.len + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for DiffOffset { + fn zero(_: &()) -> Self { + DiffOffset(0) + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + self.0 += summary.diff_map.len + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for Point { + fn zero(_: &()) -> Self { + Point::zero() + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + *self += summary.multibuffer_map.lines + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for DiffPoint { + fn zero(_: &()) -> Self { + DiffPoint(Point::zero()) + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + self.0 += summary.diff_map.lines + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for TextSummary { + fn zero(_cx: &()) -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + *self += &summary.diff_map; + } +} + +impl<'a> sum_tree::SeekTarget<'a, DiffTransformSummary, DiffTransformSummary> for DiffPoint { + fn cmp( + &self, + cursor_location: &DiffTransformSummary, + _: &::Context, + ) -> std::cmp::Ordering { + Ord::cmp(self, &cursor_location.diff_point()) + } +} + +impl<'a> sum_tree::SeekTarget<'a, DiffTransformSummary, DiffTransformSummary> for DiffOffset { + fn cmp( + &self, + cursor_location: &DiffTransformSummary, + _: &::Context, + ) -> std::cmp::Ordering { + Ord::cmp(self, &cursor_location.diff_offset()) + } +} + +impl std::ops::Add for DiffOffset { + type Output = DiffOffset; + + fn add(self, rhs: DiffOffset) -> Self::Output { + DiffOffset(self.0 + rhs.0) + } +} + +impl std::ops::AddAssign for DiffOffset { + fn add_assign(&mut self, rhs: DiffOffset) { + self.0 += rhs.0; + } +} + +impl std::ops::Sub for DiffOffset { + type Output = DiffOffset; + + fn sub(self, rhs: DiffOffset) -> Self::Output { + DiffOffset(self.0 - rhs.0) + } +} + +impl std::ops::SubAssign for DiffOffset { + fn sub_assign(&mut self, rhs: DiffOffset) { + self.0 -= rhs.0; + } +} + +impl std::ops::Add for DiffPoint { + type Output = DiffPoint; + + fn add(self, rhs: DiffPoint) -> Self::Output { + DiffPoint(self.0 + rhs.0) + } +} + +impl std::ops::AddAssign for DiffPoint { + fn add_assign(&mut self, rhs: DiffPoint) { + self.0 += rhs.0; + } +} + +impl std::ops::Sub for DiffPoint { + type Output = DiffPoint; + + fn sub(self, rhs: DiffPoint) -> Self::Output { + DiffPoint(self.0 - rhs.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::{AppContext, TestAppContext}; + use indoc::indoc; + use language::Buffer; + use multi_buffer::{Anchor, MultiBuffer}; + use project::Project; + use settings::SettingsStore; + use text::OffsetUtf16; + + #[gpui::test] + fn test_basic_diff_map_updates(cx: &mut TestAppContext) { + cx.update(init_test); + + let text = indoc!( + " + ZERO + one + TWO + three + six + " + ); + let base_text = indoc!( + " + one + two + three + four + five + six + " + ); + + let (diff_map, mut snapshot, mut deps) = build_diff_map(text, Some(base_text), cx); + assert_eq!( + snapshot.text(), + indoc!( + " + ZERO + one + TWO + three + six + " + ), + ); + + diff_map.update(cx, |diff_map, cx| { + diff_map.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + + ZERO + one + - two + + TWO + three + - four + - five + six + " + ), + ); + + assert_eq!( + snapshot + .row_infos(0) + .map(|info| info.buffer_row) + .collect::>(), + vec![ + Some(0), + Some(1), + None, + Some(2), + Some(3), + None, + None, + Some(4), + Some(5) + ] + ); + + assert_chunks_in_ranges(&snapshot); + assert_consistent_line_numbers(&snapshot); + + for (point, offset) in &[ + ( + DiffPoint::new(0, 0), + DiffOffset(snapshot.text().find("ZERO").unwrap()), + ), + ( + DiffPoint::new(2, 2), + DiffOffset(snapshot.text().find("two").unwrap() + 2), + ), + ( + DiffPoint::new(4, 3), + DiffOffset(snapshot.text().find("three").unwrap() + 3), + ), + (DiffPoint::new(8, 0), DiffOffset(snapshot.text().len())), + ] { + let actual = snapshot.point_to_offset(*point); + assert_eq!(actual, *offset, "for {:?}", point); + let actual = snapshot.offset_to_point(*offset); + assert_eq!(actual, *point, "for {:?}", offset); + } + + diff_map.update(cx, |diff_map, cx| { + diff_map.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + ZERO + one + TWO + three + six + " + ), + ); + + // Expand the first diff hunk + diff_map.update(cx, |diff_map, cx| { + let position = deps.multibuffer_snapshot.anchor_before(Point::new(2, 0)); + diff_map.expand_diff_hunks(vec![position..position], cx) + }); + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + ZERO + one + - two + + TWO + three + six + " + ), + ); + + // Expand the second diff hunk + diff_map.update(cx, |diff_map, cx| { + let position = deps.multibuffer_snapshot.anchor_before(Point::new(3, 0)); + diff_map.expand_diff_hunks(vec![position..position], cx) + }); + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + ZERO + one + - two + + TWO + three + - four + - five + six + " + ), + ); + + // Edit the buffer before the first hunk + let edits = deps.update_buffer(cx, |buffer, cx| { + buffer.edit_via_marked_text( + indoc!( + " + ZERO + one« hundred + thousand» + TWO + three + six + " + ), + None, + cx, + ); + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), edits, cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + ZERO + one hundred + thousand + - two + + TWO + three + - four + - five + six + " + ), + ); + + // Recalculate the diff, changing the first diff hunk. + let _ = deps.change_set.update(cx, |change_set, cx| { + change_set.recalculate_diff(deps.buffer.read(cx).text_snapshot(), cx) + }); + cx.run_until_parked(); + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), Vec::new(), cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + ZERO + one hundred + thousand + TWO + three + - four + - five + six + " + ), + ); + } + + #[gpui::test] + fn test_diff_map_multiple_buffer_edits(cx: &mut TestAppContext) { + cx.update(init_test); + + let text = "hello world"; + + let (diff_map, mut snapshot, mut deps) = build_diff_map(text, None, cx); + assert_eq!(snapshot.text(), "hello world"); + + let edits = deps.update_buffer(cx, |buffer, cx| { + buffer.edit([(4..5, "a"), (9..11, "k")], None, cx); + }); + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot, edits, cx) + }); + + assert_new_snapshot(&mut snapshot, sync, indoc!("hella work")); + } + + #[gpui::test] + fn test_diff_map_clipping(cx: &mut TestAppContext) { + cx.update(init_test); + + let text = indoc!( + "₀ + ₃ + ₄ + ₅ + " + ); + let base_text = indoc!( + "₀ + ₁ + ₂ + ₃ + ₄ + " + ); + + let (diff_map, mut diff_snapshot, deps) = build_diff_map(text, Some(base_text), cx); + diff_map.update(cx, |diff_map, cx| diff_map.set_all_hunks_expanded(cx)); + cx.run_until_parked(); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot, vec![], cx) + }); + + assert_new_snapshot( + &mut diff_snapshot, + sync, + indoc! {" + ₀ + - ₁ + - ₂ + ₃ + ₄ + + ₅ + "}, + ); + + for (point, (left, right)) in [ + ( + DiffPoint::new(0, 0), // start + (DiffPoint::new(0, 0), DiffPoint::new(0, 0)), + ), + ( + DiffPoint::new(1, 1), // deleted + (DiffPoint::new(1, 0), DiffPoint::new(1, 3)), + ), + ( + DiffPoint::new(3, 1), // unchanged + (DiffPoint::new(3, 0), DiffPoint::new(3, 3)), + ), + ( + DiffPoint::new(5, 2), // inserted + (DiffPoint::new(5, 0), DiffPoint::new(5, 3)), + ), + ( + DiffPoint::new(6, 0), // end + (DiffPoint::new(6, 0), DiffPoint::new(6, 0)), + ), + ( + DiffPoint::new(7, 7), // beyond + (DiffPoint::new(6, 0), DiffPoint::new(6, 0)), + ), + ] { + assert_eq!(left, diff_snapshot.clip_point(point, Bias::Left)); + assert_eq!(right, diff_snapshot.clip_point(point, Bias::Right)); + } + + assert_eq!( + diff_snapshot.text_summary_for_range(DiffOffset(0)..DiffOffset(0)), + TextSummary::default() + ); + assert_eq!( + diff_snapshot.text_summary_for_range(diff_snapshot.len()..diff_snapshot.len()), + TextSummary::default() + ); + let full_summary = TextSummary { + len: 24, + len_utf16: OffsetUtf16(12), + lines: Point { row: 6, column: 0 }, + first_line_chars: 1, + last_line_chars: 0, + last_line_len_utf16: 0, + longest_row: 0, + longest_row_chars: 1, + }; + let partial_summary = TextSummary { + len: 8, + len_utf16: OffsetUtf16(4), + lines: Point { row: 2, column: 0 }, + first_line_chars: 1, + last_line_chars: 0, + last_line_len_utf16: 0, + longest_row: 0, + longest_row_chars: 1, + }; + + let two = DiffOffset(diff_snapshot.text().find("₂").unwrap()); + let four = DiffOffset(diff_snapshot.text().find("₄").unwrap()); + + assert_eq!( + diff_snapshot.text_summary_for_range(DiffOffset(0)..diff_snapshot.len()), + full_summary + ); + assert_eq!( + diff_snapshot.text_summary_for_range(DiffOffset(0)..two), + partial_summary + ); + assert_eq!( + diff_snapshot.text_summary_for_range(two..four), + partial_summary + ); + assert_eq!( + diff_snapshot.text_summary_for_range(four..diff_snapshot.len()), + partial_summary + ); + } + + #[gpui::test] + fn test_empty_diff_map(cx: &mut TestAppContext) { + cx.update(init_test); + + let (_diff_map, diff_snapshot, _deps) = build_diff_map("", None, cx); + assert_eq!( + diff_snapshot + .row_infos(0) + .map(|info| info.buffer_row) + .collect::>(), + [Some(0)] + ); + } + + #[gpui::test] + fn test_expand_no_newline(cx: &mut TestAppContext) { + cx.update(init_test); + + let base_text = "todo"; + let text = indoc!( + " + one + two + " + ); + + let (diff_map, mut diff_snapshot, deps) = build_diff_map(text, Some(base_text), cx); + assert_eq!(diff_snapshot.text(), "one\ntwo\n"); + assert_eq!( + diff_snapshot + .row_infos(0) + .map(|info| info.buffer_row) + .collect::>(), + [Some(0), Some(1), Some(2)] + ); + + diff_map.update(cx, |diff_map, cx| { + diff_map.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut diff_snapshot, + sync, + indoc!( + " + - todo + + one + + two + " + ), + ); + + assert_eq!(diff_snapshot.text(), "todo\none\ntwo\n"); + + assert_eq!( + diff_snapshot + .row_infos(0) + .map(|info| info.buffer_row) + .collect::>(), + [None, Some(0), Some(1), Some(2)] + ); + assert_eq!( + diff_snapshot + .row_infos(1) + .map(|info| info.buffer_row) + .collect::>(), + [Some(0), Some(1), Some(2)] + ); + + for (point, offset) in [ + (DiffPoint::new(0, 4), DiffOffset(4)), + (DiffPoint::new(1, 0), DiffOffset(5)), + (DiffPoint::new(1, 1), DiffOffset(6)), + ] { + assert_eq!(diff_snapshot.offset_to_point(offset), point); + assert_eq!(diff_snapshot.point_to_offset(point), offset); + } + + for point in [ + DiffPoint::new(0, 4), + DiffPoint::new(1, 0), + DiffPoint::new(1, 1), + ] { + let anchor = diff_snapshot.point_to_anchor(point, Bias::Left); + dbg!(&anchor); + let actual = diff_snapshot.anchor_to_point(anchor); + assert_eq!(point, actual); + } + + assert_eq!( + diff_snapshot.clip_point(DiffPoint::new(0, 5), Bias::Left), + DiffPoint::new(0, 4) + ); + assert_eq!( + diff_snapshot.to_diff_point(Point::new(0, 0)), + DiffPoint::new(1, 0) + ); + assert_eq!( + diff_snapshot.to_multibuffer_point(DiffPoint::new(0, 4)), + Point::new(0, 0) + ); + } + + #[gpui::test] + fn test_expand_collapse_at_positions_adjacent_to_hunks(cx: &mut TestAppContext) { + cx.update(init_test); + + let base_text = indoc!( + " + one + two + three + four + five + six + seven + eight + " + ); + let text = indoc!( + " + one + two + five + six; seven + eight + " + ); + + let (diff_map, mut snapshot, deps) = build_diff_map(text, Some(base_text), cx); + + // Expand at the line right below a deleted hunk. + diff_map.update(cx, |diff_map, cx| { + let point = deps.multibuffer_snapshot.anchor_before(Point::new(2, 0)); + diff_map.expand_diff_hunks(vec![point..point], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + - three + - four + five + six; seven + eight + " + ), + ); + + // Collapse at the line right below a deleted hunk. + diff_map.update(cx, |diff_map, cx| { + let point = deps.multibuffer_snapshot.anchor_before(Point::new(2, 0)); + diff_map.collapse_diff_hunks(vec![point..point], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + five + six; seven + eight + " + ), + ); + + // Expand at the line right above a deleted hunk. + diff_map.update(cx, |diff_map, cx| { + let point = deps.multibuffer_snapshot.anchor_before(Point::new(1, 0)); + diff_map.expand_diff_hunks(vec![point..point], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + - three + - four + five + six; seven + eight + " + ), + ); + + eprintln!(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + + // Expand at the line right below a modified hunk. Should not expand anything. + diff_map.update(cx, |diff_map, cx| { + let point = deps.multibuffer_snapshot.anchor_before(Point::new(4, 0)); + diff_map.expand_diff_hunks(vec![point..point], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + - three + - four + five + six; seven + eight + " + ), + ); + } + + #[gpui::test] + fn test_expand_collapse_insertion_hunk(cx: &mut TestAppContext) { + cx.update(init_test); + + let base_text = indoc!( + " + one + two + seven + eight + " + ); + let text = indoc!( + " + one + two + three + four + five + six + seven + eight + " + ); + + let (diff_map, mut snapshot, deps) = build_diff_map(text, Some(base_text), cx); + + // Expand at the line right right after a deleted hunk. + diff_map.update(cx, |diff_map, cx| { + let point = deps.multibuffer_snapshot.anchor_before(Point::new(2, 0)); + diff_map.expand_diff_hunks(vec![point..point], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + + three + + four + + five + + six + seven + eight + " + ), + ); + + diff_map.update(cx, |diff_map, cx| { + let point = deps.multibuffer_snapshot.anchor_before(Point::new(2, 0)); + diff_map.collapse_diff_hunks(vec![point..point], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + three + four + five + six + seven + eight + " + ), + ); + } + + #[gpui::test] + fn test_edit_in_insertion_hunk(cx: &mut TestAppContext) { + cx.update(init_test); + + let base_text = indoc!( + " + one + two + six + seven + " + ); + let text = indoc!( + " + one + two + three + four + five + six + seven + " + ); + + let (diff_map, mut snapshot, mut deps) = build_diff_map(text, Some(base_text), cx); + + // Expand the hunk + diff_map.update(cx, |diff_map, cx| { + diff_map.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + + three + + four + + five + six + seven + " + ), + ); + + eprintln!(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + + let edits = deps.update_buffer(cx, |buffer, cx| { + buffer.edit_via_marked_text( + indoc!( + " + one + two + three« + !» + four + five + six + seven + " + ), + None, + cx, + ) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), edits, cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + + three + + ! + + four + + five + six + seven + " + ), + ); + } + + // #[track_caller] + fn assert_new_snapshot( + snapshot: &mut DiffMapSnapshot, + (new_snapshot, edits): (DiffMapSnapshot, Vec>), + expected_diff: &str, + ) { + let actual_text = new_snapshot.text(); + let line_infos = new_snapshot.row_infos(0).collect::>(); + let has_diff = line_infos.iter().any(|info| info.diff_status.is_some()); + let actual_diff = actual_text + .split('\n') + .zip(line_infos) + .map(|(line, info)| { + let marker = match info.diff_status { + Some(DiffHunkStatus::Added) => "+ ", + Some(DiffHunkStatus::Removed) => "- ", + Some(DiffHunkStatus::Modified) => unreachable!(), + None => { + if has_diff { + " " + } else { + "" + } + } + }; + if line.is_empty() { + String::new() + } else { + format!("{marker}{line}") + } + }) + .collect::>() + .join("\n"); + pretty_assertions::assert_eq!(actual_diff, expected_diff); + check_edits(snapshot, &new_snapshot, &edits); + *snapshot = new_snapshot; + } + + #[track_caller] + fn check_edits( + old_snapshot: &DiffMapSnapshot, + new_snapshot: &DiffMapSnapshot, + edits: &[DiffEdit], + ) { + let mut text = old_snapshot.text(); + let new_text = new_snapshot.text(); + for edit in edits.iter().rev() { + if !text.is_char_boundary(edit.old.start.0) + || !text.is_char_boundary(edit.old.end.0) + || !new_text.is_char_boundary(edit.new.start.0) + || !new_text.is_char_boundary(edit.new.end.0) + { + panic!( + "invalid edits: {:?}\nold text: {:?}\nnew text: {:?}", + edits, text, new_text + ); + } + + text.replace_range( + edit.old.start.0..edit.old.end.0, + &new_text[edit.new.start.0..edit.new.end.0], + ); + } + + pretty_assertions::assert_eq!(text, new_text, "invalid edits: {:?}", edits); + } + + #[track_caller] + fn assert_chunks_in_ranges(snapshot: &DiffMapSnapshot) { + let full_text = snapshot.text(); + for ix in 0..full_text.len() { + let offset = DiffOffset(ix); + let mut chunks = + snapshot.chunks(DiffOffset(0)..snapshot.len(), false, Default::default()); + chunks.seek(offset..snapshot.len()); + let tail = chunks.map(|chunk| chunk.text).collect::(); + assert_eq!(tail, &full_text[ix..], "seek to range: {:?}", ix..); + + let tail = snapshot + .chunks(offset..snapshot.len(), false, Default::default()) + .map(|chunk| chunk.text) + .collect::(); + assert_eq!(tail, &full_text[ix..], "start from range: {:?}", ix..); + + let head = snapshot + .chunks(DiffOffset(0)..offset, false, Default::default()) + .map(|chunk| chunk.text) + .collect::(); + assert_eq!(head, &full_text[..ix], "start with range: {:?}", ..ix); + } + } + + #[track_caller] + fn assert_consistent_line_numbers(snapshot: &DiffMapSnapshot) { + let all_line_numbers = snapshot.row_infos(0).collect::>(); + for start_row in 1..all_line_numbers.len() { + let line_numbers = snapshot.row_infos(start_row as u32).collect::>(); + assert_eq!( + line_numbers, + all_line_numbers[start_row..], + "start_row: {start_row}" + ); + + for seek_row in 0..all_line_numbers.len() { + let mut numbers = snapshot.row_infos(start_row as u32); + numbers.seek(seek_row as u32); + let line_numbers = numbers.collect::>(); + assert_eq!( + line_numbers, + all_line_numbers[seek_row..], + "seek_row: {seek_row}, start_row: {start_row}" + ); + } + } + } + + struct DiffMapDeps { + buffer: Model, + multibuffer: Model, + change_set: Model, + multibuffer_snapshot: MultiBufferSnapshot, + multibuffer_edits: text::Subscription, + } + + fn build_diff_map( + text: &str, + base_text: Option<&str>, + cx: &mut TestAppContext, + ) -> (Model, DiffMapSnapshot, DiffMapDeps) { + let buffer = cx.new_model(|cx| Buffer::local(text, cx)); + + let change_set = cx.new_model(|cx| { + let text_snapshot = buffer.read(cx).text_snapshot(); + let mut change_set = BufferChangeSet::new(&text_snapshot); + if let Some(base_text) = base_text { + let _ = change_set.set_base_text(base_text.to_string(), text_snapshot, cx); + } + change_set + }); + + let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); + + let (multibuffer_snapshot, multibuffer_edits) = + multibuffer.update(cx, |buffer, cx| (buffer.snapshot(cx), buffer.subscribe())); + + let (diff_map, diff_map_snapshot) = cx.update(|cx| DiffMap::new(multibuffer.clone(), cx)); + diff_map.update(cx, |diff_map, cx| { + diff_map.add_change_set(change_set.clone(), cx) + }); + cx.run_until_parked(); + + ( + diff_map, + diff_map_snapshot, + DiffMapDeps { + buffer, + multibuffer, + change_set, + multibuffer_snapshot, + multibuffer_edits, + }, + ) + } + + impl DiffMapDeps { + fn update_buffer( + &mut self, + cx: &mut TestAppContext, + f: impl FnOnce(&mut Buffer, &mut ModelContext), + ) -> Vec> { + self.buffer.update(cx, f); + + self.multibuffer_snapshot = self + .multibuffer + .read_with(cx, |buffer, cx| buffer.snapshot(cx)); + self.multibuffer_edits.consume().into_inner() + } + } + + fn init_test(cx: &mut AppContext) { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + language::init(cx); + crate::init(cx); + Project::init_settings(cx); + } +} diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index c4bb4080e2613..0ac726a5554e7 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,3 +1,5 @@ +use crate::RowInfo; + use super::{ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, Highlights, @@ -139,7 +141,7 @@ impl<'a> FoldMapWriter<'a> { let mut folds = Vec::new(); let snapshot = self.0.snapshot.inlay_snapshot.clone(); for (range, fold_text) in ranges.into_iter() { - let buffer = &snapshot.buffer; + let buffer = snapshot.buffer(); let range = range.start.to_offset(buffer)..range.end.to_offset(buffer); // Ignore any empty ranges. @@ -161,14 +163,14 @@ impl<'a> FoldMapWriter<'a> { }); let inlay_range = - snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end); + snapshot.make_inlay_offset(range.start)..snapshot.make_inlay_offset(range.end); edits.push(InlayEdit { old: inlay_range.clone(), new: inlay_range, }); } - let buffer = &snapshot.buffer; + let buffer = snapshot.buffer(); folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(&a.range, &b.range, buffer)); self.0.snapshot.folds = { @@ -220,7 +222,7 @@ impl<'a> FoldMapWriter<'a> { let mut edits = Vec::new(); let mut fold_ixs_to_delete = Vec::new(); let snapshot = self.0.snapshot.inlay_snapshot.clone(); - let buffer = &snapshot.buffer; + let buffer = snapshot.buffer(); for range in ranges.into_iter() { let range = range.start.to_offset(buffer)..range.end.to_offset(buffer); let mut folds_cursor = @@ -230,8 +232,8 @@ impl<'a> FoldMapWriter<'a> { fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer); if should_unfold(fold) { if offset_range.end > offset_range.start { - let inlay_range = snapshot.to_inlay_offset(offset_range.start) - ..snapshot.to_inlay_offset(offset_range.end); + let inlay_range = snapshot.make_inlay_offset(offset_range.start) + ..snapshot.make_inlay_offset(offset_range.end); edits.push(InlayEdit { old: inlay_range.clone(), new: inlay_range, @@ -275,7 +277,7 @@ impl FoldMap { pub(crate) fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) { let this = Self { snapshot: FoldSnapshot { - folds: SumTree::new(&inlay_snapshot.buffer), + folds: SumTree::new(inlay_snapshot.buffer()), transforms: SumTree::from_item( Transform { summary: TransformSummary { @@ -336,9 +338,7 @@ impl FoldMap { let mut folds = self.snapshot.folds.iter().peekable(); while let Some(fold) = folds.next() { if let Some(next_fold) = folds.peek() { - let comparison = fold - .range - .cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer); + let comparison = fold.range.cmp(&next_fold.range, self.snapshot.buffer()); assert!(comparison.is_le()); } } @@ -410,31 +410,31 @@ impl FoldMap { InlayOffset(((edit.new.start + edit.old_len()).0 as isize + delta) as usize); let anchor = inlay_snapshot - .buffer + .buffer() .anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start)); let mut folds_cursor = self .snapshot .folds - .cursor::(&inlay_snapshot.buffer); + .cursor::(&inlay_snapshot.buffer()); folds_cursor.seek( &FoldRange(anchor..Anchor::max()), Bias::Left, - &inlay_snapshot.buffer, + &inlay_snapshot.buffer(), ); let mut folds = iter::from_fn({ let inlay_snapshot = &inlay_snapshot; move || { let item = folds_cursor.item().map(|fold| { - let buffer_start = fold.range.start.to_offset(&inlay_snapshot.buffer); - let buffer_end = fold.range.end.to_offset(&inlay_snapshot.buffer); + let buffer_start = fold.range.start.to_offset(&inlay_snapshot.buffer()); + let buffer_end = fold.range.end.to_offset(&inlay_snapshot.buffer()); ( fold.clone(), - inlay_snapshot.to_inlay_offset(buffer_start) - ..inlay_snapshot.to_inlay_offset(buffer_end), + inlay_snapshot.make_inlay_offset(buffer_start) + ..inlay_snapshot.make_inlay_offset(buffer_end), ) }); - folds_cursor.next(&inlay_snapshot.buffer); + folds_cursor.next(&inlay_snapshot.buffer()); item } }) @@ -578,6 +578,10 @@ pub struct FoldSnapshot { } impl FoldSnapshot { + pub fn buffer(&self) -> &MultiBufferSnapshot { + self.inlay_snapshot.buffer() + } + #[cfg(test)] pub fn text(&self) -> String { self.chunks(FoldOffset(0)..self.len(), false, Highlights::default()) @@ -587,7 +591,7 @@ impl FoldSnapshot { #[cfg(test)] pub fn fold_count(&self) -> usize { - self.folds.items(&self.inlay_snapshot.buffer).len() + self.folds.items(self.inlay_snapshot.buffer()).len() } pub fn text_summary_for_range(&self, range: Range) -> TextSummary { @@ -641,6 +645,32 @@ impl FoldSnapshot { summary } + pub fn make_fold_point(&self, point: Point, bias: Bias) -> FoldPoint { + self.to_fold_point(self.inlay_snapshot.make_inlay_point(point), bias) + } + + pub fn make_fold_offset(&self, buffer_offset: usize, bias: Bias) -> FoldOffset { + self.to_fold_offset(self.inlay_snapshot.make_inlay_offset(buffer_offset), bias) + } + + pub fn to_fold_offset(&self, inlay_offset: InlayOffset, bias: Bias) -> FoldOffset { + let mut cursor = self.transforms.cursor::<(InlayOffset, FoldOffset)>(&()); + cursor.seek(&inlay_offset, Bias::Right, &()); + if cursor.item().map_or(false, |t| t.is_fold()) { + if bias == Bias::Left || inlay_offset == cursor.start().0 { + cursor.start().1 + } else { + cursor.end(&()).1 + } + } else { + let overshoot = inlay_offset.0 - cursor.start().0 .0; + FoldOffset(cmp::min( + cursor.start().1 .0 + overshoot, + cursor.end(&()).1 .0, + )) + } + } + pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint { let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>(&()); cursor.seek(&point, Bias::Right, &()); @@ -673,7 +703,7 @@ impl FoldSnapshot { (line_end - line_start) as u32 } - pub fn buffer_rows(&self, start_row: u32) -> FoldBufferRows { + pub fn row_infos(&self, start_row: u32) -> FoldRows { if start_row > self.transforms.summary().output.lines.row { panic!("invalid display row {}", start_row); } @@ -684,11 +714,11 @@ impl FoldSnapshot { let overshoot = fold_point.0 - cursor.start().0 .0; let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot); - let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row()); + let input_rows = self.inlay_snapshot.row_infos(inlay_point.row()); - FoldBufferRows { + FoldRows { fold_point, - input_buffer_rows, + input_rows, cursor, } } @@ -706,12 +736,12 @@ impl FoldSnapshot { where T: ToOffset, { - let buffer = &self.inlay_snapshot.buffer; + let buffer = self.inlay_snapshot.buffer(); let range = range.start.to_offset(buffer)..range.end.to_offset(buffer); let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false); iter::from_fn(move || { let item = folds.item(); - folds.next(&self.inlay_snapshot.buffer); + folds.next(buffer); item }) } @@ -720,8 +750,8 @@ impl FoldSnapshot { where T: ToOffset, { - let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer); - let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset); + let buffer_offset = offset.to_offset(self.inlay_snapshot.buffer()); + let inlay_offset = self.inlay_snapshot.make_inlay_offset(buffer_offset); let mut cursor = self.transforms.cursor::(&()); cursor.seek(&inlay_offset, Bias::Right, &()); cursor.item().map_or(false, |t| t.placeholder.is_some()) @@ -730,7 +760,7 @@ impl FoldSnapshot { pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool { let mut inlay_point = self .inlay_snapshot - .to_inlay_point(Point::new(buffer_row.0, 0)); + .make_inlay_point(Point::new(buffer_row.0, 0)); let mut cursor = self.transforms.cursor::(&()); cursor.seek(&inlay_point, Bias::Right, &()); loop { @@ -870,7 +900,7 @@ fn intersecting_folds<'a>( range: Range, inclusive: bool, ) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize> { - let buffer = &inlay_snapshot.buffer; + let buffer = inlay_snapshot.buffer(); let start = buffer.anchor_before(range.start.to_offset(buffer)); let end = buffer.anchor_after(range.end.to_offset(buffer)); let mut cursor = folds.filter::<_, usize>(buffer, move |summary| { @@ -1134,25 +1164,25 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize { } #[derive(Clone)] -pub struct FoldBufferRows<'a> { +pub struct FoldRows<'a> { cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>, - input_buffer_rows: InlayBufferRows<'a>, + input_rows: InlayBufferRows<'a>, fold_point: FoldPoint, } -impl<'a> FoldBufferRows<'a> { +impl<'a> FoldRows<'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.input_rows.seek(inlay_point.row()); self.fold_point = fold_point; } } -impl<'a> Iterator for FoldBufferRows<'a> { - type Item = Option; +impl<'a> Iterator for FoldRows<'a> { + type Item = RowInfo; fn next(&mut self) -> Option { let mut traversed_fold = false; @@ -1166,11 +1196,11 @@ impl<'a> Iterator for FoldBufferRows<'a> { if self.cursor.item().is_some() { if traversed_fold { - self.input_buffer_rows.seek(self.cursor.start().1.row()); - self.input_buffer_rows.next(); + self.input_rows.seek(self.cursor.start().1 .0.row); + self.input_rows.next(); } *self.fold_point.row_mut() += 1; - self.input_buffer_rows.next() + self.input_rows.next() } else { None } @@ -1380,7 +1410,10 @@ pub type FoldEdit = Edit; #[cfg(test)] mod tests { use super::*; - use crate::{display_map::inlay_map::InlayMap, MultiBuffer, ToPoint}; + use crate::{ + display_map::{diff_map::DiffMap, inlay_map::InlayMap}, + MultiBuffer, ToPoint, + }; use collections::HashSet; use rand::prelude::*; use settings::SettingsStore; @@ -1395,8 +1428,8 @@ mod tests { init_test(cx); let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); - let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (diff_map, diff_snapshot) = DiffMap::new(buffer.clone(), cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot, vec![]); @@ -1431,8 +1464,14 @@ mod tests { buffer.snapshot(cx) }); - let (inlay_snapshot, inlay_edits) = - inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync( + buffer_snapshot.clone(), + subscription.consume().into_inner(), + cx, + ) + }); + let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits); let (snapshot3, edits) = map.read(inlay_snapshot, inlay_edits); assert_eq!(snapshot3.text(), "123a⋯c123c⋯eeeee"); assert_eq!( @@ -1453,8 +1492,11 @@ mod tests { buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], None, cx); buffer.snapshot(cx) }); - let (inlay_snapshot, inlay_edits) = - inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer_snapshot, subscription.consume().into_inner(), cx) + }); + + let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits); let (snapshot4, _) = map.read(inlay_snapshot.clone(), inlay_edits); assert_eq!(snapshot4.text(), "123a⋯c123456eee"); @@ -1474,8 +1516,8 @@ mod tests { init_test(cx); let buffer = MultiBuffer::build_simple("abcdefghijkl", cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); - let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (diff_map, diff_snapshot) = DiffMap::new(buffer.clone(), cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); { let mut map = FoldMap::new(inlay_snapshot.clone()).0; @@ -1521,8 +1563,10 @@ mod tests { buffer.edit([(0..1, "12345")], None, cx); buffer.snapshot(cx) }); - let (inlay_snapshot, inlay_edits) = - inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer_snapshot, subscription.consume().into_inner(), cx) + }); + let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits); let (snapshot, _) = map.read(inlay_snapshot, inlay_edits); assert_eq!(snapshot.text(), "12345⋯fghijkl"); } @@ -1531,8 +1575,8 @@ mod tests { #[gpui::test] fn test_overlapping_folds(cx: &mut gpui::AppContext) { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); - let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); + let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx); + let (_, inlay_snapshot) = InlayMap::new(diff_snapshot); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![ @@ -1550,8 +1594,8 @@ mod tests { init_test(cx); let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); - let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (diff_map, diff_snapshot) = DiffMap::new(buffer.clone(), cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); @@ -1566,8 +1610,10 @@ mod tests { buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx); buffer.snapshot(cx) }); - let (inlay_snapshot, inlay_edits) = - inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer_snapshot, subscription.consume().into_inner(), cx) + }); + let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits); let (snapshot, _) = map.read(inlay_snapshot, inlay_edits); assert_eq!(snapshot.text(), "aa⋯eeeee"); } @@ -1576,7 +1622,8 @@ mod tests { fn test_folds_in_range(cx: &mut gpui::AppContext) { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx); + let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); @@ -1618,7 +1665,8 @@ mod tests { MultiBuffer::build_random(&mut rng, cx) }; let mut buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (diff_map, diff_snapshot) = DiffMap::new(buffer.clone(), cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); @@ -1648,8 +1696,10 @@ mod tests { }), }; - let (inlay_snapshot, new_inlay_edits) = - inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer_snapshot.clone(), buffer_edits, cx) + }); + let (inlay_snapshot, new_inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits); log::info!("inlay text {:?}", inlay_snapshot.text()); let inlay_edits = Patch::new(inlay_edits) @@ -1660,8 +1710,8 @@ mod tests { let mut expected_text: String = inlay_snapshot.text().to_string(); for fold_range in map.merged_folds().into_iter().rev() { - let fold_inlay_start = inlay_snapshot.to_inlay_offset(fold_range.start); - let fold_inlay_end = inlay_snapshot.to_inlay_offset(fold_range.end); + let fold_inlay_start = inlay_snapshot.make_inlay_offset(fold_range.start); + let fold_inlay_end = inlay_snapshot.make_inlay_offset(fold_range.end); expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "⋯"); } @@ -1676,19 +1726,19 @@ mod tests { let mut expected_buffer_rows = Vec::new(); for fold_range in map.merged_folds() { let fold_start = inlay_snapshot - .to_point(inlay_snapshot.to_inlay_offset(fold_range.start)) + .to_point(inlay_snapshot.make_inlay_offset(fold_range.start)) .row(); let fold_end = inlay_snapshot - .to_point(inlay_snapshot.to_inlay_offset(fold_range.end)) + .to_point(inlay_snapshot.make_inlay_offset(fold_range.end)) .row(); expected_buffer_rows.extend( inlay_snapshot - .buffer_rows(prev_row) + .row_infos(prev_row) .take((1 + fold_start - prev_row) as usize), ); prev_row = 1 + fold_end; } - expected_buffer_rows.extend(inlay_snapshot.buffer_rows(prev_row)); + expected_buffer_rows.extend(inlay_snapshot.row_infos(prev_row)); assert_eq!( expected_buffer_rows.len(), @@ -1777,7 +1827,7 @@ mod tests { let mut fold_row = 0; while fold_row < expected_buffer_rows.len() as u32 { assert_eq!( - snapshot.buffer_rows(fold_row).collect::>(), + snapshot.row_infos(fold_row).collect::>(), expected_buffer_rows[(fold_row as usize)..], "wrong buffer rows starting at fold row {}", fold_row, @@ -1879,8 +1929,8 @@ mod tests { let text = sample_text(6, 6, 'a') + "\n"; let buffer = MultiBuffer::build_simple(&text, cx); - let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); + let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx); + let (_, inlay_snapshot) = InlayMap::new(diff_snapshot); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); @@ -1892,10 +1942,19 @@ mod tests { let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee\nffffff\n"); assert_eq!( - snapshot.buffer_rows(0).collect::>(), + snapshot + .row_infos(0) + .map(|info| info.buffer_row) + .collect::>(), [Some(0), Some(3), Some(5), Some(6)] ); - assert_eq!(snapshot.buffer_rows(3).collect::>(), [Some(6)]); + assert_eq!( + snapshot + .row_infos(3) + .map(|info| info.buffer_row) + .collect::>(), + [Some(6)] + ); } fn init_test(cx: &mut gpui::AppContext) { @@ -1906,7 +1965,7 @@ mod tests { impl FoldMap { fn merged_folds(&self) -> Vec> { let inlay_snapshot = self.snapshot.inlay_snapshot.clone(); - let buffer = &inlay_snapshot.buffer; + let buffer = inlay_snapshot.buffer(); let mut folds = self.snapshot.folds.items(buffer); // Ensure sorting doesn't change how folds get merged and displayed. folds.sort_by(|a, b| a.range.cmp(&b.range, buffer)); @@ -1942,7 +2001,7 @@ mod tests { match rng.gen_range(0..=100) { 0..=39 if !self.snapshot.folds.is_empty() => { let inlay_snapshot = self.snapshot.inlay_snapshot.clone(); - let buffer = &inlay_snapshot.buffer; + let buffer = inlay_snapshot.buffer(); let mut to_unfold = Vec::new(); for _ in 0..rng.gen_range(1..=3) { let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); @@ -1958,7 +2017,7 @@ mod tests { } _ => { let inlay_snapshot = self.snapshot.inlay_snapshot.clone(); - let buffer = &inlay_snapshot.buffer; + let buffer = inlay_snapshot.buffer(); let mut to_fold = Vec::new(); for _ in 0..rng.gen_range(1..=2) { let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index e288f5a76c90d..bbf1270d9df5c 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,7 +1,7 @@ -use crate::{HighlightStyles, InlayId}; +use crate::{HighlightStyles, InlayId, RowInfo}; use collections::BTreeSet; use language::{Chunk, Edit, Point, TextSummary}; -use multi_buffer::{Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, ToOffset}; +use multi_buffer::{Anchor, MultiBufferSnapshot, ToOffset}; use std::{ cmp, ops::{Add, AddAssign, Range, Sub, SubAssign}, @@ -9,7 +9,10 @@ use std::{ use sum_tree::{Bias, Cursor, SumTree}; use text::{Patch, Rope}; -use super::{custom_highlights::CustomHighlightsChunks, Highlights}; +use super::{ + diff_map::{DiffEdit, DiffMapChunks, DiffMapRows, DiffMapSnapshot, DiffOffset, DiffPoint}, + Highlights, +}; /// Decides where the [`Inlay`]s should be displayed. /// @@ -21,7 +24,7 @@ pub struct InlayMap { #[derive(Clone)] pub struct InlaySnapshot { - pub buffer: MultiBufferSnapshot, + pub diff_map_snapshot: DiffMapSnapshot, transforms: SumTree, pub version: usize, } @@ -172,37 +175,37 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint { } } -impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize { +impl<'a> sum_tree::Dimension<'a, TransformSummary> for DiffOffset { fn zero(_cx: &()) -> Self { - Default::default() + DiffOffset(0) } fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { - *self += &summary.input.len; + self.0 += &summary.input.len; } } -impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point { +impl<'a> sum_tree::Dimension<'a, TransformSummary> for DiffPoint { fn zero(_cx: &()) -> Self { Default::default() } fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { - *self += &summary.input.lines; + self.0 += &summary.input.lines; } } #[derive(Clone)] pub struct InlayBufferRows<'a> { - transforms: Cursor<'a, Transform, (InlayPoint, Point)>, - buffer_rows: MultiBufferRows<'a>, + transforms: Cursor<'a, Transform, (InlayPoint, DiffPoint)>, + diff_rows: DiffMapRows<'a>, inlay_row: u32, - max_buffer_row: MultiBufferRow, + max_point: DiffPoint, } pub struct InlayChunks<'a> { - transforms: Cursor<'a, Transform, (InlayOffset, usize)>, - buffer_chunks: CustomHighlightsChunks<'a>, + transforms: Cursor<'a, Transform, (InlayOffset, DiffOffset)>, + diff_map_chunks: DiffMapChunks<'a>, buffer_chunk: Option>, inlay_chunks: Option>, inlay_chunk: Option<&'a str>, @@ -214,21 +217,21 @@ pub struct InlayChunks<'a> { } impl<'a> InlayChunks<'a> { + pub fn offset(&self) -> InlayOffset { + self.output_offset + } + pub fn seek(&mut self, new_range: Range) { self.transforms.seek(&new_range.start, Bias::Right, &()); - let buffer_range = self.snapshot.to_buffer_offset(new_range.start) - ..self.snapshot.to_buffer_offset(new_range.end); - self.buffer_chunks.seek(buffer_range); + let diff_range = self.snapshot.to_diff_offset(new_range.start) + ..self.snapshot.to_diff_offset(new_range.end); + self.diff_map_chunks.seek(diff_range); self.inlay_chunks = None; self.buffer_chunk = None; self.output_offset = new_range.start; self.max_output_offset = new_range.end; } - - pub fn offset(&self) -> InlayOffset { - self.output_offset - } } impl<'a> Iterator for InlayChunks<'a> { @@ -243,9 +246,9 @@ impl<'a> Iterator for InlayChunks<'a> { Transform::Isomorphic(_) => { let chunk = self .buffer_chunk - .get_or_insert_with(|| self.buffer_chunks.next().unwrap()); + .get_or_insert_with(|| self.diff_map_chunks.next().unwrap()); if chunk.text.is_empty() { - *chunk = self.buffer_chunks.next().unwrap(); + *chunk = self.diff_map_chunks.next().unwrap(); } let (prefix, suffix) = chunk.text.split_at( @@ -344,33 +347,33 @@ impl<'a> InlayBufferRows<'a> { let inlay_point = InlayPoint::new(row, 0); self.transforms.seek(&inlay_point, Bias::Left, &()); - let mut buffer_point = self.transforms.start().1; - let buffer_row = MultiBufferRow(if row == 0 { + let mut diff_point = self.transforms.start().1; + let row = if row == 0 { 0 } else { match self.transforms.item() { Some(Transform::Isomorphic(_)) => { - buffer_point += inlay_point.0 - self.transforms.start().0 .0; - buffer_point.row + diff_point.0 += inlay_point.0 - self.transforms.start().0 .0; + diff_point.row() } - _ => cmp::min(buffer_point.row + 1, self.max_buffer_row.0), + _ => cmp::min(diff_point.row() + 1, self.max_point.row()), } - }); + }; self.inlay_row = inlay_point.row(); - self.buffer_rows.seek(buffer_row); + self.diff_rows.seek(row); } } impl<'a> Iterator for InlayBufferRows<'a> { - type Item = Option; + type Item = RowInfo; fn next(&mut self) -> Option { - let buffer_row = if self.inlay_row == 0 { - self.buffer_rows.next().unwrap() + let row_info = if self.inlay_row == 0 { + self.diff_rows.next().unwrap() } else { match self.transforms.item()? { - Transform::Inlay(_) => None, - Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(), + Transform::Inlay(_) => Default::default(), + Transform::Isomorphic(_) => self.diff_rows.next().unwrap(), } }; @@ -378,7 +381,7 @@ impl<'a> Iterator for InlayBufferRows<'a> { self.transforms .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left, &()); - Some(buffer_row) + Some(row_info) } } @@ -393,11 +396,14 @@ impl InlayPoint { } impl InlayMap { - pub fn new(buffer: MultiBufferSnapshot) -> (Self, InlaySnapshot) { + pub fn new(diff_map_snapshot: DiffMapSnapshot) -> (Self, InlaySnapshot) { let version = 0; let snapshot = InlaySnapshot { - buffer: buffer.clone(), - transforms: SumTree::from_iter(Some(Transform::Isomorphic(buffer.text_summary())), &()), + transforms: SumTree::from_iter( + Some(Transform::Isomorphic(diff_map_snapshot.text_summary())), + &(), + ), + diff_map_snapshot, version, }; @@ -412,133 +418,121 @@ impl InlayMap { pub fn sync( &mut self, - buffer_snapshot: MultiBufferSnapshot, - mut buffer_edits: Vec>, + diff_map_snapshot: DiffMapSnapshot, + mut diff_edits: Vec, ) -> (InlaySnapshot, Vec) { let snapshot = &mut self.snapshot; - if buffer_edits.is_empty() - && snapshot.buffer.trailing_excerpt_update_count() - != buffer_snapshot.trailing_excerpt_update_count() + if diff_edits.is_empty() + && snapshot.buffer().trailing_excerpt_update_count() + != diff_map_snapshot.buffer().trailing_excerpt_update_count() { - buffer_edits.push(Edit { - old: snapshot.buffer.len()..snapshot.buffer.len(), - new: buffer_snapshot.len()..buffer_snapshot.len(), + diff_edits.push(Edit { + old: snapshot.diff_map_snapshot.len()..snapshot.diff_map_snapshot.len(), + new: diff_map_snapshot.len()..diff_map_snapshot.len(), }); } - if buffer_edits.is_empty() { - if snapshot.buffer.edit_count() != buffer_snapshot.edit_count() - || snapshot.buffer.non_text_state_update_count() - != buffer_snapshot.non_text_state_update_count() - || snapshot.buffer.trailing_excerpt_update_count() - != buffer_snapshot.trailing_excerpt_update_count() - { - snapshot.version += 1; - } - - snapshot.buffer = buffer_snapshot; - (snapshot.clone(), Vec::new()) - } else { - let mut inlay_edits = Patch::default(); - let mut new_transforms = SumTree::default(); - let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>(&()); - let mut buffer_edits_iter = buffer_edits.iter().peekable(); - while let Some(buffer_edit) = buffer_edits_iter.next() { - new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &()); - if let Some(Transform::Isomorphic(transform)) = cursor.item() { - if cursor.end(&()).0 == buffer_edit.old.start { - push_isomorphic(&mut new_transforms, transform.clone()); - cursor.next(&()); - } + let mut inlay_edits = Patch::default(); + let mut new_transforms = SumTree::default(); + let mut cursor = snapshot.transforms.cursor::<(DiffOffset, InlayOffset)>(&()); + let mut diff_edits_iter = diff_edits.iter().peekable(); + while let Some(diff_edit) = diff_edits_iter.next() { + new_transforms.append(cursor.slice(&diff_edit.old.start, Bias::Left, &()), &()); + if let Some(Transform::Isomorphic(transform)) = cursor.item() { + if cursor.end(&()).0 == diff_edit.old.start { + push_isomorphic(&mut new_transforms, transform.clone()); + cursor.next(&()); } + } - // Remove all the inlays and transforms contained by the edit. - let old_start = - cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0); - cursor.seek(&buffer_edit.old.end, Bias::Right, &()); - let old_end = - cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0); - - // Push the unchanged prefix. - let prefix_start = new_transforms.summary().input.len; - let prefix_end = buffer_edit.new.start; - push_isomorphic( - &mut new_transforms, - buffer_snapshot.text_summary_for_range(prefix_start..prefix_end), - ); - let new_start = InlayOffset(new_transforms.summary().output.len); - - let start_ix = match self.inlays.binary_search_by(|probe| { - probe - .position - .to_offset(&buffer_snapshot) - .cmp(&buffer_edit.new.start) - .then(std::cmp::Ordering::Greater) - }) { - Ok(ix) | Err(ix) => ix, - }; + // Remove all the inlays and transforms contained by the edit. + let old_start = + cursor.start().1 + InlayOffset((diff_edit.old.start - cursor.start().0).0); + cursor.seek(&diff_edit.old.end, Bias::Right, &()); + let old_end = cursor.start().1 + InlayOffset((diff_edit.old.end - cursor.start().0).0); + + // Push the unchanged prefix. + let prefix_start = DiffOffset(new_transforms.summary().input.len); + let prefix_end = diff_edit.new.start; + push_isomorphic( + &mut new_transforms, + diff_map_snapshot.text_summary_for_range(prefix_start..prefix_end), + ); + let new_start = InlayOffset(new_transforms.summary().output.len); - for inlay in &self.inlays[start_ix..] { - let buffer_offset = inlay.position.to_offset(&buffer_snapshot); - if buffer_offset > buffer_edit.new.end { - break; - } + let edit_buffer_range = diff_map_snapshot.to_multibuffer_offset(diff_edit.new.start) + ..diff_map_snapshot.to_multibuffer_offset(diff_edit.new.end); - let prefix_start = new_transforms.summary().input.len; - let prefix_end = buffer_offset; - push_isomorphic( - &mut new_transforms, - buffer_snapshot.text_summary_for_range(prefix_start..prefix_end), - ); + let start_ix = match self.inlays.binary_search_by(|probe| { + probe + .position + .to_offset(diff_map_snapshot.buffer()) + .cmp(&edit_buffer_range.start) + .then(std::cmp::Ordering::Greater) + }) { + Ok(ix) | Err(ix) => ix, + }; - if inlay.position.is_valid(&buffer_snapshot) { - new_transforms.push(Transform::Inlay(inlay.clone()), &()); - } + for inlay in &self.inlays[start_ix..] { + let buffer_offset = inlay.position.to_offset(&diff_map_snapshot.buffer); + let diff_offset = diff_map_snapshot.to_diff_offset(buffer_offset); + if buffer_offset > edit_buffer_range.end { + break; } - // Apply the rest of the edit. - let transform_start = new_transforms.summary().input.len; + let prefix_start = DiffOffset(new_transforms.summary().input.len); + let prefix_end = diff_offset; push_isomorphic( &mut new_transforms, - buffer_snapshot.text_summary_for_range(transform_start..buffer_edit.new.end), + diff_map_snapshot.text_summary_for_range(prefix_start..prefix_end), ); - let new_end = InlayOffset(new_transforms.summary().output.len); - inlay_edits.push(Edit { - old: old_start..old_end, - new: new_start..new_end, - }); - // If the next edit doesn't intersect the current isomorphic transform, then - // we can push its remainder. - if buffer_edits_iter - .peek() - .map_or(true, |edit| edit.old.start >= cursor.end(&()).0) - { - let transform_start = new_transforms.summary().input.len; - let transform_end = - buffer_edit.new.end + (cursor.end(&()).0 - buffer_edit.old.end); - push_isomorphic( - &mut new_transforms, - buffer_snapshot.text_summary_for_range(transform_start..transform_end), - ); - cursor.next(&()); + if inlay.position.is_valid(&diff_map_snapshot.buffer) { + new_transforms.push(Transform::Inlay(inlay.clone()), &()); } } - new_transforms.append(cursor.suffix(&()), &()); - if new_transforms.is_empty() { - new_transforms.push(Transform::Isomorphic(Default::default()), &()); - } + // Apply the rest of the edit. + let transform_start = DiffOffset(new_transforms.summary().input.len); + push_isomorphic( + &mut new_transforms, + diff_map_snapshot.text_summary_for_range(transform_start..diff_edit.new.end), + ); + let new_end = InlayOffset(new_transforms.summary().output.len); + inlay_edits.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); - drop(cursor); - snapshot.transforms = new_transforms; - snapshot.version += 1; - snapshot.buffer = buffer_snapshot; - snapshot.check_invariants(); + // If the next edit doesn't intersect the current isomorphic transform, then + // we can push its remainder. + if diff_edits_iter + .peek() + .map_or(true, |edit| edit.old.start >= cursor.end(&()).0) + { + let transform_start = DiffOffset(new_transforms.summary().input.len); + let transform_end = diff_edit.new.end + (cursor.end(&()).0 - diff_edit.old.end); + push_isomorphic( + &mut new_transforms, + diff_map_snapshot.text_summary_for_range(transform_start..transform_end), + ); + cursor.next(&()); + } + } - (snapshot.clone(), inlay_edits.into_inner()) + new_transforms.append(cursor.suffix(&()), &()); + if new_transforms.is_empty() { + new_transforms.push(Transform::Isomorphic(Default::default()), &()); } + + drop(cursor); + snapshot.transforms = new_transforms; + snapshot.version += 1; + snapshot.diff_map_snapshot = diff_map_snapshot; + snapshot.check_invariants(); + + (snapshot.clone(), inlay_edits.into_inner()) } pub fn splice( @@ -552,7 +546,9 @@ impl InlayMap { self.inlays.retain(|inlay| { let retain = !to_remove.contains(&inlay.id); if !retain { - let offset = inlay.position.to_offset(&snapshot.buffer); + let offset = snapshot + .diff_map_snapshot + .to_diff_offset(inlay.position.to_offset(&snapshot.diff_map_snapshot.buffer)); edits.insert(offset); } retain @@ -564,11 +560,16 @@ impl InlayMap { continue; } - let offset = inlay_to_insert.position.to_offset(&snapshot.buffer); - match self.inlays.binary_search_by(|probe| { - probe + let offset = snapshot.diff_map_snapshot.to_diff_offset( + inlay_to_insert .position - .cmp(&inlay_to_insert.position, &snapshot.buffer) + .to_offset(&snapshot.diff_map_snapshot.buffer), + ); + match self.inlays.binary_search_by(|probe| { + probe.position.cmp( + &inlay_to_insert.position, + &snapshot.diff_map_snapshot.buffer, + ) }) { Ok(ix) | Err(ix) => { self.inlays.insert(ix, inlay_to_insert); @@ -585,7 +586,7 @@ impl InlayMap { new: offset..offset, }) .collect(); - let buffer_snapshot = snapshot.buffer.clone(); + let buffer_snapshot = snapshot.diff_map_snapshot.clone(); let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits); (snapshot, edits) } @@ -608,7 +609,8 @@ impl InlayMap { let snapshot = &mut self.snapshot; for i in 0..rng.gen_range(1..=5) { if self.inlays.is_empty() || rng.gen() { - let position = snapshot.buffer.random_byte_range(0, rng).start; + let position = snapshot.buffer().random_byte_range(0, rng).start; + let anchor = snapshot.buffer().anchor_at(position, Bias::Left); let bias = if rng.gen() { Bias::Left } else { Bias::Right }; let len = if rng.gen_bool(0.01) { 0 @@ -626,7 +628,7 @@ impl InlayMap { InlayId::InlineCompletion(post_inc(next_inlay_id)) }; log::info!( - "creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}", + "creating inlay {:?} at buffer offset {:?} with bias {:?} and text {:?}", inlay_id, position, bias, @@ -635,7 +637,7 @@ impl InlayMap { to_insert.push(Inlay { id: inlay_id, - position: snapshot.buffer.anchor_at(position, bias), + position: anchor, text: text.into(), }); } else { @@ -659,16 +661,16 @@ impl InlaySnapshot { pub fn to_point(&self, offset: InlayOffset) -> InlayPoint { let mut cursor = self .transforms - .cursor::<(InlayOffset, (InlayPoint, usize))>(&()); + .cursor::<(InlayOffset, (InlayPoint, DiffOffset))>(&()); cursor.seek(&offset, Bias::Right, &()); let overshoot = offset.0 - cursor.start().0 .0; match cursor.item() { Some(Transform::Isomorphic(_)) => { let buffer_offset_start = cursor.start().1 .1; - let buffer_offset_end = buffer_offset_start + overshoot; - let buffer_start = self.buffer.offset_to_point(buffer_offset_start); - let buffer_end = self.buffer.offset_to_point(buffer_offset_end); - InlayPoint(cursor.start().1 .0 .0 + (buffer_end - buffer_start)) + let buffer_offset_end = buffer_offset_start + DiffOffset(overshoot); + let buffer_start = self.diff_map_snapshot.offset_to_point(buffer_offset_start); + let buffer_end = self.diff_map_snapshot.offset_to_point(buffer_offset_end); + cursor.start().1 .0 + InlayPoint(buffer_end.0 - buffer_start.0) } Some(Transform::Inlay(inlay)) => { let overshoot = inlay.text.offset_to_point(overshoot); @@ -689,51 +691,68 @@ impl InlaySnapshot { pub fn to_offset(&self, point: InlayPoint) -> InlayOffset { let mut cursor = self .transforms - .cursor::<(InlayPoint, (InlayOffset, Point))>(&()); + .cursor::<(InlayPoint, (InlayOffset, DiffPoint))>(&()); cursor.seek(&point, Bias::Right, &()); let overshoot = point.0 - cursor.start().0 .0; match cursor.item() { Some(Transform::Isomorphic(_)) => { let buffer_point_start = cursor.start().1 .1; - let buffer_point_end = buffer_point_start + overshoot; - let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start); - let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end); - InlayOffset(cursor.start().1 .0 .0 + (buffer_offset_end - buffer_offset_start)) + let buffer_point_end = buffer_point_start + DiffPoint(overshoot); + let buffer_offset_start = + self.diff_map_snapshot.point_to_offset(buffer_point_start); + let buffer_offset_end = self.diff_map_snapshot.point_to_offset(buffer_point_end); + cursor.start().1 .0 + InlayOffset((buffer_offset_end - buffer_offset_start).0) } Some(Transform::Inlay(inlay)) => { let overshoot = inlay.text.point_to_offset(overshoot); - InlayOffset(cursor.start().1 .0 .0 + overshoot) + cursor.start().1 .0 + InlayOffset(overshoot) } None => self.len(), } } - pub fn to_buffer_point(&self, point: InlayPoint) -> Point { - let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); + + pub fn to_diff_point(&self, point: InlayPoint) -> DiffPoint { + let mut cursor = self.transforms.cursor::<(InlayPoint, DiffPoint)>(&()); cursor.seek(&point, Bias::Right, &()); match cursor.item() { Some(Transform::Isomorphic(_)) => { let overshoot = point.0 - cursor.start().0 .0; - cursor.start().1 + overshoot + cursor.start().1 + DiffPoint(overshoot) } Some(Transform::Inlay(_)) => cursor.start().1, - None => self.buffer.max_point(), + None => self.diff_map_snapshot.max_point(), } } - pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize { - let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); + + pub fn to_diff_offset(&self, offset: InlayOffset) -> DiffOffset { + let mut cursor = self.transforms.cursor::<(InlayOffset, DiffOffset)>(&()); cursor.seek(&offset, Bias::Right, &()); match cursor.item() { Some(Transform::Isomorphic(_)) => { let overshoot = offset - cursor.start().0; - cursor.start().1 + overshoot.0 + cursor.start().1 + DiffOffset(overshoot.0) } Some(Transform::Inlay(_)) => cursor.start().1, - None => self.buffer.len(), + None => self.diff_map_snapshot.len(), } } - pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset { - let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>(&()); + pub fn buffer(&self) -> &MultiBufferSnapshot { + &self.diff_map_snapshot.buffer + } + + pub fn to_buffer_point(&self, point: InlayPoint) -> Point { + self.diff_map_snapshot + .to_multibuffer_point(self.to_diff_point(point)) + } + + pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize { + self.diff_map_snapshot + .to_multibuffer_offset(self.to_diff_offset(offset)) + } + + pub fn to_inlay_offset(&self, offset: DiffOffset) -> InlayOffset { + let mut cursor = self.transforms.cursor::<(DiffOffset, InlayOffset)>(&()); cursor.seek(&offset, Bias::Left, &()); loop { match cursor.item() { @@ -748,7 +767,7 @@ impl InlaySnapshot { } return cursor.end(&()).1; } else { - let overshoot = offset - cursor.start().0; + let overshoot = offset.0 - cursor.start().0 .0; return InlayOffset(cursor.start().1 .0 + overshoot); } } @@ -765,8 +784,17 @@ impl InlaySnapshot { } } } - pub fn to_inlay_point(&self, point: Point) -> InlayPoint { - let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>(&()); + + pub fn make_inlay_point(&self, multibuffer_point: Point) -> InlayPoint { + self.to_inlay_point(self.diff_map_snapshot.to_diff_point(multibuffer_point)) + } + + pub fn make_inlay_offset(&self, multibuffer_point: usize) -> InlayOffset { + self.to_inlay_offset(self.diff_map_snapshot.to_diff_offset(multibuffer_point)) + } + + pub fn to_inlay_point(&self, point: DiffPoint) -> InlayPoint { + let mut cursor = self.transforms.cursor::<(DiffPoint, InlayPoint)>(&()); cursor.seek(&point, Bias::Left, &()); loop { match cursor.item() { @@ -782,7 +810,7 @@ impl InlaySnapshot { return cursor.end(&()).1; } else { let overshoot = point - cursor.start().0; - return InlayPoint(cursor.start().1 .0 + overshoot); + return InlayPoint(cursor.start().1 .0 + overshoot.0); } } Some(Transform::Inlay(inlay)) => { @@ -800,7 +828,7 @@ impl InlaySnapshot { } pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint { - let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); + let mut cursor = self.transforms.cursor::<(InlayPoint, DiffPoint)>(&()); cursor.seek(&point, Bias::Left, &()); loop { match cursor.item() { @@ -836,10 +864,11 @@ impl InlaySnapshot { } } else { let overshoot = point.0 - cursor.start().0 .0; - let buffer_point = cursor.start().1 + overshoot; - let clipped_buffer_point = self.buffer.clip_point(buffer_point, bias); - let clipped_overshoot = clipped_buffer_point - cursor.start().1; - let clipped_point = InlayPoint(cursor.start().0 .0 + clipped_overshoot); + let diff_point = cursor.start().1 + DiffPoint(overshoot); + let clipped_diff_point = + self.diff_map_snapshot.clip_point(diff_point, bias); + let clipped_overshoot = clipped_diff_point - cursor.start().1; + let clipped_point = cursor.start().0 + InlayPoint(clipped_overshoot.0); if clipped_point == point { return clipped_point; } else { @@ -897,17 +926,19 @@ impl InlaySnapshot { pub fn text_summary_for_range(&self, range: Range) -> TextSummary { let mut summary = TextSummary::default(); - let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); + let mut cursor = self.transforms.cursor::<(InlayOffset, DiffOffset)>(&()); cursor.seek(&range.start, Bias::Right, &()); let overshoot = range.start.0 - cursor.start().0 .0; match cursor.item() { Some(Transform::Isomorphic(_)) => { let buffer_start = cursor.start().1; - let suffix_start = buffer_start + overshoot; - let suffix_end = - buffer_start + (cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0); - summary = self.buffer.text_summary_for_range(suffix_start..suffix_end); + let suffix_start = buffer_start + DiffOffset(overshoot); + let suffix_end = buffer_start + + DiffOffset(cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0); + summary = self + .diff_map_snapshot + .text_summary_for_range(suffix_start..suffix_end); cursor.next(&()); } Some(Transform::Inlay(inlay)) => { @@ -928,10 +959,10 @@ impl InlaySnapshot { match cursor.item() { Some(Transform::Isomorphic(_)) => { let prefix_start = cursor.start().1; - let prefix_end = prefix_start + overshoot; + let prefix_end = prefix_start + DiffOffset(overshoot); summary += self - .buffer - .text_summary_for_range::(prefix_start..prefix_end); + .diff_map_snapshot + .text_summary_for_range(prefix_start..prefix_end); } Some(Transform::Inlay(inlay)) => { let prefix_end = overshoot; @@ -944,30 +975,30 @@ impl InlaySnapshot { summary } - pub fn buffer_rows(&self, row: u32) -> InlayBufferRows<'_> { - let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); + pub fn row_infos(&self, row: u32) -> InlayBufferRows<'_> { + let mut cursor = self.transforms.cursor::<(InlayPoint, DiffPoint)>(&()); let inlay_point = InlayPoint::new(row, 0); cursor.seek(&inlay_point, Bias::Left, &()); - let max_buffer_row = self.buffer.max_row(); - let mut buffer_point = cursor.start().1; - let buffer_row = if row == 0 { - MultiBufferRow(0) + let max_point = self.diff_map_snapshot.max_point(); + let mut diff_point = cursor.start().1; + let diff_row = if row == 0 { + 0 } else { match cursor.item() { Some(Transform::Isomorphic(_)) => { - buffer_point += inlay_point.0 - cursor.start().0 .0; - MultiBufferRow(buffer_point.row) + diff_point += DiffPoint(inlay_point.0 - cursor.start().0 .0); + diff_point.row() } - _ => cmp::min(MultiBufferRow(buffer_point.row + 1), max_buffer_row), + _ => cmp::min(diff_point.row() + 1, max_point.row()), } }; InlayBufferRows { transforms: cursor, inlay_row: inlay_point.row(), - buffer_rows: self.buffer.buffer_rows(buffer_row), - max_buffer_row, + diff_rows: self.diff_map_snapshot.row_infos(diff_row), + max_point, } } @@ -987,20 +1018,17 @@ impl InlaySnapshot { language_aware: bool, highlights: Highlights<'a>, ) -> InlayChunks<'a> { - let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); + let mut cursor = self.transforms.cursor::<(InlayOffset, DiffOffset)>(&()); cursor.seek(&range.start, Bias::Right, &()); - let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); - let buffer_chunks = CustomHighlightsChunks::new( - buffer_range, - language_aware, - highlights.text_highlights, - &self.buffer, - ); + let diff_range = self.to_diff_offset(range.start)..self.to_diff_offset(range.end); + let diff_map_chunks = + self.diff_map_snapshot + .chunks(diff_range, language_aware, highlights.text_highlights); InlayChunks { transforms: cursor, - buffer_chunks, + diff_map_chunks, inlay_chunks: None, inlay_chunk: None, buffer_chunk: None, @@ -1022,7 +1050,10 @@ impl InlaySnapshot { fn check_invariants(&self) { #[cfg(any(debug_assertions, feature = "test-support"))] { - assert_eq!(self.transforms.summary().input, self.buffer.text_summary()); + assert_eq!( + self.transforms.summary().input, + self.diff_map_snapshot.text_summary() + ); let mut transforms = self.transforms.iter().peekable(); while let Some(transform) = transforms.next() { let transform_is_isomorphic = matches!(transform, Transform::Isomorphic(_)); @@ -1063,7 +1094,7 @@ fn push_isomorphic(sum_tree: &mut SumTree, summary: TextSummary) { mod tests { use super::*; use crate::{ - display_map::{InlayHighlights, TextHighlights}, + display_map::{DiffMap, InlayHighlights, TextHighlights}, hover_links::InlayHighlight, InlayId, MultiBuffer, }; @@ -1163,7 +1194,8 @@ mod tests { fn test_basic_inlays(cx: &mut AppContext) { let buffer = MultiBuffer::build_simple("abcdefghi", cx); let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe()); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx)); + let (diff_map, diff_map_snapshot) = DiffMap::new(buffer.clone(), cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_map_snapshot.clone()); assert_eq!(inlay_snapshot.text(), "abcdefghi"); let mut next_inlay_id = 0; @@ -1177,27 +1209,27 @@ mod tests { ); assert_eq!(inlay_snapshot.text(), "abc|123|defghi"); assert_eq!( - inlay_snapshot.to_inlay_point(Point::new(0, 0)), + inlay_snapshot.make_inlay_point(Point::new(0, 0)), InlayPoint::new(0, 0) ); assert_eq!( - inlay_snapshot.to_inlay_point(Point::new(0, 1)), + inlay_snapshot.make_inlay_point(Point::new(0, 1)), InlayPoint::new(0, 1) ); assert_eq!( - inlay_snapshot.to_inlay_point(Point::new(0, 2)), + inlay_snapshot.make_inlay_point(Point::new(0, 2)), InlayPoint::new(0, 2) ); assert_eq!( - inlay_snapshot.to_inlay_point(Point::new(0, 3)), + inlay_snapshot.make_inlay_point(Point::new(0, 3)), InlayPoint::new(0, 3) ); assert_eq!( - inlay_snapshot.to_inlay_point(Point::new(0, 4)), + inlay_snapshot.make_inlay_point(Point::new(0, 4)), InlayPoint::new(0, 9) ); assert_eq!( - inlay_snapshot.to_inlay_point(Point::new(0, 5)), + inlay_snapshot.make_inlay_point(Point::new(0, 5)), InlayPoint::new(0, 10) ); assert_eq!( @@ -1229,18 +1261,26 @@ mod tests { buffer.update(cx, |buffer, cx| { buffer.edit([(2..3, "x"), (3..3, "y"), (4..4, "z")], None, cx) }); - let (inlay_snapshot, _) = inlay_map.sync( - buffer.read(cx).snapshot(cx), - buffer_edits.consume().into_inner(), - ); + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync( + buffer.read(cx).snapshot(cx), + buffer_edits.consume().into_inner(), + cx, + ) + }); + let (inlay_snapshot, _) = inlay_map.sync(diff_snapshot, diff_edits); assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi"); // An edit surrounding the inlay should invalidate it. buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "D")], None, cx)); - let (inlay_snapshot, _) = inlay_map.sync( - buffer.read(cx).snapshot(cx), - buffer_edits.consume().into_inner(), - ); + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync( + buffer.read(cx).snapshot(cx), + buffer_edits.consume().into_inner(), + cx, + ) + }); + let (inlay_snapshot, _) = inlay_map.sync(diff_snapshot, diff_edits); assert_eq!(inlay_snapshot.text(), "abxyDzefghi"); let (inlay_snapshot, _) = inlay_map.splice( @@ -1262,10 +1302,16 @@ mod tests { // Edits ending where the inlay starts should not move it if it has a left bias. buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "JKL")], None, cx)); - let (inlay_snapshot, _) = inlay_map.sync( - buffer.read(cx).snapshot(cx), - buffer_edits.consume().into_inner(), - ); + + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync( + buffer.read(cx).snapshot(cx), + buffer_edits.consume().into_inner(), + cx, + ) + }); + + let (inlay_snapshot, _) = inlay_map.sync(diff_snapshot, diff_edits); assert_eq!(inlay_snapshot.text(), "abx|123|JKL|456|yDzefghi"); assert_eq!( @@ -1450,7 +1496,8 @@ mod tests { #[gpui::test] fn test_inlay_buffer_rows(cx: &mut AppContext) { let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx)); + let (_diff_map, diff_map_snapshot) = DiffMap::new(buffer.clone(), cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_map_snapshot); assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi"); let mut next_inlay_id = 0; @@ -1476,7 +1523,10 @@ mod tests { ); assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi"); assert_eq!( - inlay_snapshot.buffer_rows(0).collect::>(), + inlay_snapshot + .row_infos(0) + .map(|r| r.buffer_row) + .collect::>(), vec![Some(0), None, Some(1), None, None, Some(2)] ); } @@ -1501,7 +1551,8 @@ mod tests { let mut buffer_snapshot = buffer.read(cx).snapshot(cx); let mut next_inlay_id = 0; log::info!("buffer text: {:?}", buffer_snapshot.text()); - let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (diff_map, diff_map_snapshot) = DiffMap::new(buffer.clone(), cx); + let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(diff_map_snapshot.clone()); for _ in 0..operations { let mut inlay_edits = Patch::default(); @@ -1524,8 +1575,12 @@ mod tests { }), }; + let (new_diff_snapshot, new_diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer.read(cx).snapshot(cx), buffer_edits, cx) + }); + let (new_inlay_snapshot, new_inlay_edits) = - inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + inlay_map.sync(new_diff_snapshot, new_diff_edits); inlay_snapshot = new_inlay_snapshot; inlay_edits = inlay_edits.compose(new_inlay_edits); @@ -1547,7 +1602,7 @@ mod tests { } assert_eq!(inlay_snapshot.text(), expected_text.to_string()); - let expected_buffer_rows = inlay_snapshot.buffer_rows(0).collect::>(); + let expected_buffer_rows = inlay_snapshot.row_infos(0).collect::>(); assert_eq!( expected_buffer_rows.len() as u32, expected_text.max_point().row + 1 @@ -1555,7 +1610,7 @@ mod tests { for row_start in 0..expected_buffer_rows.len() { assert_eq!( inlay_snapshot - .buffer_rows(row_start as u32) + .row_infos(row_start as u32) .collect::>(), &expected_buffer_rows[row_start..], "incorrect buffer rows starting at {}", @@ -1673,14 +1728,14 @@ mod tests { assert_eq!(expected_text.len(), inlay_snapshot.len().0); let mut buffer_point = Point::default(); - let mut inlay_point = inlay_snapshot.to_inlay_point(buffer_point); + let mut inlay_point = inlay_snapshot.make_inlay_point(buffer_point); let mut buffer_chars = buffer_snapshot.chars_at(0); loop { // Ensure conversion from buffer coordinates to inlay coordinates // is consistent. let buffer_offset = buffer_snapshot.point_to_offset(buffer_point); assert_eq!( - inlay_snapshot.to_point(inlay_snapshot.to_inlay_offset(buffer_offset)), + inlay_snapshot.to_point(inlay_snapshot.make_inlay_offset(buffer_offset)), inlay_point ); @@ -1707,7 +1762,7 @@ mod tests { } // Ensure that moving forward in the buffer always moves the inlay point forward as well. - let new_inlay_point = inlay_snapshot.to_inlay_point(buffer_point); + let new_inlay_point = inlay_snapshot.make_inlay_point(buffer_point); assert!(new_inlay_point > inlay_point); inlay_point = new_inlay_point; } else { @@ -1767,7 +1822,7 @@ mod tests { // Ensure the clipped points are at valid buffer locations. assert_eq!( inlay_snapshot - .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_left_point)), + .to_inlay_point(inlay_snapshot.to_diff_point(clipped_left_point)), clipped_left_point, "to_buffer_point({:?}) = {:?}", clipped_left_point, @@ -1775,7 +1830,7 @@ mod tests { ); assert_eq!( inlay_snapshot - .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_right_point)), + .to_inlay_point(inlay_snapshot.to_diff_point(clipped_right_point)), clipped_right_point, "to_buffer_point({:?}) = {:?}", clipped_right_point, diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 86fa492712a06..22e923e30eb22 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -164,7 +164,7 @@ pub struct TabSnapshot { impl TabSnapshot { pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot { - &self.fold_snapshot.inlay_snapshot.buffer + self.fold_snapshot.inlay_snapshot.buffer() } pub fn line_len(&self, row: u32) -> u32 { @@ -272,8 +272,8 @@ impl TabSnapshot { } } - pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> { - self.fold_snapshot.buffer_rows(row) + pub fn rows(&self, row: u32) -> fold_map::FoldRows<'_> { + self.fold_snapshot.row_infos(row) } #[cfg(test)] @@ -317,8 +317,7 @@ impl TabSnapshot { } pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint { - let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point); - let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); + let fold_point = self.fold_snapshot.make_fold_point(point, bias); self.to_tab_point(fold_point) } @@ -602,7 +601,7 @@ impl<'a> Iterator for TabChunks<'a> { mod tests { use super::*; use crate::{ - display_map::{fold_map::FoldMap, inlay_map::InlayMap}, + display_map::{diff_map::DiffMap, fold_map::FoldMap, inlay_map::InlayMap}, MultiBuffer, }; use rand::{prelude::StdRng, Rng}; @@ -610,8 +609,8 @@ mod tests { #[gpui::test] fn test_expand_tabs(cx: &mut gpui::AppContext) { let buffer = MultiBuffer::build_simple("", cx); - let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, diff_snapshot) = DiffMap::new(buffer, cx); + let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -627,8 +626,8 @@ mod tests { let output = "A BC DEF G HI J K L M"; let buffer = MultiBuffer::build_simple(input, cx); - let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, diff_snapshot) = DiffMap::new(buffer, cx); + let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -674,8 +673,8 @@ mod tests { let input = "abcdefg⋯hij"; let buffer = MultiBuffer::build_simple(input, cx); - let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx); + let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -688,8 +687,8 @@ mod tests { let input = "\t \thello"; let buffer = MultiBuffer::build_simple(input, cx); - let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx); + let (_, inlay_snapshot) = InlayMap::new(diff_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -749,8 +748,11 @@ mod tests { let buffer_snapshot = buffer.read(cx).snapshot(cx); log::info!("Buffer text: {:?}", buffer_snapshot.text()); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); - log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx); + + let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone()); + + log::info!("DiffMap text: {:?}", diff_snapshot.text()); let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone()); fold_map.randomly_mutate(&mut rng); let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]); diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index ceb91ce0ab24c..d7fbec9879a72 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,5 +1,7 @@ +use crate::RowInfo; + use super::{ - fold_map::FoldBufferRows, + fold_map::FoldRows, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, Highlights, }; @@ -60,16 +62,16 @@ pub struct WrapChunks<'a> { } #[derive(Clone)] -pub struct WrapBufferRows<'a> { - input_buffer_rows: FoldBufferRows<'a>, - input_buffer_row: Option, +pub struct WrapRows<'a> { + input_buffer_rows: FoldRows<'a>, + input_buffer_row: RowInfo, output_row: u32, soft_wrapped: bool, max_output_row: u32, transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, } -impl<'a> WrapBufferRows<'a> { +impl<'a> WrapRows<'a> { pub(crate) fn seek(&mut self, start_row: u32) { self.transforms .seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); @@ -717,7 +719,7 @@ impl WrapSnapshot { self.transforms.summary().output.longest_row } - pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows { + pub fn row_infos(&self, start_row: u32) -> WrapRows { let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); let mut input_row = transforms.start().1.row(); @@ -725,9 +727,9 @@ impl WrapSnapshot { input_row += start_row - transforms.start().0.row(); } let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic()); - let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row); + let mut input_buffer_rows = self.tab_snapshot.rows(input_row); let input_buffer_row = input_buffer_rows.next().unwrap(); - WrapBufferRows { + WrapRows { transforms, input_buffer_row, input_buffer_rows, @@ -847,7 +849,7 @@ impl WrapSnapshot { } let text = language::Rope::from(self.text().as_str()); - let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0); + let mut input_buffer_rows = self.tab_snapshot.rows(0); let mut expected_buffer_rows = Vec::new(); let mut prev_tab_row = 0; for display_row in 0..=self.max_point().row() { @@ -855,7 +857,7 @@ impl WrapSnapshot { if tab_point.row() == prev_tab_row && display_row != 0 { expected_buffer_rows.push(None); } else { - expected_buffer_rows.push(input_buffer_rows.next().unwrap()); + expected_buffer_rows.push(input_buffer_rows.next().unwrap().buffer_row); } prev_tab_row = tab_point.row(); @@ -864,7 +866,8 @@ impl WrapSnapshot { for start_display_row in 0..expected_buffer_rows.len() { assert_eq!( - self.buffer_rows(start_display_row as u32) + self.row_infos(start_display_row as u32) + .map(|row_info| row_info.buffer_row) .collect::>(), &expected_buffer_rows[start_display_row..], "invalid buffer_rows({}..)", @@ -958,8 +961,8 @@ impl<'a> Iterator for WrapChunks<'a> { } } -impl<'a> Iterator for WrapBufferRows<'a> { - type Item = Option; +impl<'a> Iterator for WrapRows<'a> { + type Item = RowInfo; fn next(&mut self) -> Option { if self.output_row > self.max_output_row { @@ -979,7 +982,11 @@ impl<'a> Iterator for WrapBufferRows<'a> { self.soft_wrapped = true; } - Some(if soft_wrapped { None } else { buffer_row }) + Some(if soft_wrapped { + RowInfo::default() + } else { + buffer_row + }) } } @@ -1161,7 +1168,7 @@ fn consolidate_wrap_edits(edits: Vec) -> Vec { mod tests { use super::*; use crate::{ - display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, + display_map::{diff_map::DiffMap, fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, MultiBuffer, }; use gpui::{font, px, test::observe}; @@ -1209,9 +1216,11 @@ mod tests { }); let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); log::info!("Buffer text: {:?}", buffer_snapshot.text()); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (diff_map, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx)); + log::info!("DiffMap text: {:?}", diff_snapshot.text()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot); log::info!("InlayMap text: {:?}", inlay_snapshot.text()); - let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); log::info!("FoldMap text: {:?}", fold_snapshot.text()); let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); let tabs_snapshot = tab_map.set_max_expansion_column(32); @@ -1293,8 +1302,10 @@ mod tests { } log::info!("Buffer text: {:?}", buffer_snapshot.text()); - let (inlay_snapshot, inlay_edits) = - inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(buffer_snapshot.clone(), buffer_edits, cx) + }); + let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits); log::info!("InlayMap text: {:?}", inlay_snapshot.text()); let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); log::info!("FoldMap text: {:?}", fold_snapshot.text()); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4265a5760c933..207f5823b9f0e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -89,7 +89,7 @@ use gpui::{ use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; pub(crate) use hunk_diff::HoveredHunk; -use hunk_diff::{diff_hunk_to_display, DiffMap, DiffMapSnapshot}; +use hunk_diff::{diff_hunk_to_display, DiffMap}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; pub use inline_completion::Direction; @@ -725,7 +725,6 @@ pub struct EditorSnapshot { git_blame_gutter_max_author_length: Option, pub display_snapshot: DisplaySnapshot, pub placeholder_text: Option>, - diff_map: DiffMapSnapshot, is_focused: bool, scroll_anchor: ScrollAnchor, ongoing_scroll: OngoingScroll, @@ -1205,7 +1204,12 @@ impl Editor { let mut code_action_providers = Vec::new(); if let Some(project) = project.clone() { - get_unstaged_changes_for_buffers(&project, buffer.read(cx).all_buffers(), cx); + get_unstaged_changes_for_buffers( + &project, + buffer.read(cx).all_buffers(), + display_map.clone(), + cx, + ); code_action_providers.push(Rc::new(project) as Rc<_>); } @@ -1568,7 +1572,6 @@ impl Editor { scroll_anchor: self.scroll_manager.anchor(), ongoing_scroll: self.scroll_manager.ongoing_scroll(), placeholder_text: self.placeholder_text.clone(), - diff_map: self.diff_map.snapshot(), is_focused: self.focus_handle.is_focused(cx), current_line_highlight: self .current_line_highlight @@ -5082,7 +5085,7 @@ impl Editor { })) } - #[cfg(feature = "test-support")] + #[cfg(any(test, feature = "test-support"))] pub fn context_menu_visible(&self) -> bool { self.context_menu .borrow() @@ -5918,10 +5921,9 @@ impl Editor { pub fn revert_file(&mut self, _: &RevertFile, cx: &mut ViewContext) { let mut revert_changes = HashMap::default(); let snapshot = self.snapshot(cx); - for hunk in hunks_for_ranges( - Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter(), - &snapshot, - ) { + for hunk in snapshot + .hunks_for_ranges(Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter()) + { self.prepare_revert_change(&mut revert_changes, &hunk, cx); } if !revert_changes.is_empty() { @@ -5939,7 +5941,12 @@ impl Editor { } pub fn revert_selected_hunks(&mut self, _: &RevertSelectedHunks, cx: &mut ViewContext) { - let revert_changes = self.gather_revert_changes(&self.selections.all(cx), cx); + let selections = self.selections.all(cx).into_iter().map(|s| s.range()); + let mut revert_changes = HashMap::default(); + let snapshot = self.snapshot(cx); + for hunk in &snapshot.hunks_for_ranges(selections) { + self.prepare_revert_change(&mut revert_changes, &hunk, cx); + } if !revert_changes.is_empty() { self.transact(cx, |editor, cx| { editor.revert(revert_changes, cx); @@ -5976,28 +5983,18 @@ impl Editor { } } - fn gather_revert_changes( - &mut self, - selections: &[Selection], - cx: &mut ViewContext<'_, Editor>, - ) -> HashMap, Rope)>> { - let mut revert_changes = HashMap::default(); - let snapshot = self.snapshot(cx); - for hunk in hunks_for_selections(&snapshot, selections) { - self.prepare_revert_change(&mut revert_changes, &hunk, cx); - } - revert_changes - } - pub fn prepare_revert_change( &mut self, revert_changes: &mut HashMap, Rope)>>, hunk: &MultiBufferDiffHunk, - cx: &AppContext, + cx: &mut WindowContext, ) -> Option<()> { + let change_set = self + .display_map + .read(cx) + .diff_base_for(hunk.buffer_id, cx)?; let buffer = self.buffer.read(cx).buffer(hunk.buffer_id)?; let buffer = buffer.read(cx); - let change_set = &self.diff_map.diff_bases.get(&hunk.buffer_id)?.change_set; let original_text = change_set .read(cx) .base_text @@ -9213,9 +9210,8 @@ impl Editor { snapshot, position, ix > 0, - snapshot.diff_map.diff_hunks_in_range( + snapshot.diff_hunks_in_range( position + Point::new(1, 0)..snapshot.buffer_snapshot.max_point(), - &snapshot.buffer_snapshot, ), cx, ) { @@ -9245,9 +9241,7 @@ impl Editor { snapshot, position, ix > 0, - snapshot - .diff_map - .diff_hunks_in_range_rev(Point::zero()..position, &snapshot.buffer_snapshot), + snapshot.diff_hunks_in_range_rev(Point::zero()..position), cx, ) { return Some(hunk); @@ -10930,6 +10924,29 @@ impl Editor { self.display_map.read(cx).fold_placeholder.clone() } + pub fn set_expand_all_diff_hunks(&mut self, cx: &mut AppContext) { + self.display_map.update(cx, |display_map, cx| { + display_map.set_all_hunks_expanded(cx); + }); + } + + pub fn expand_all_diff_hunks(&mut self, _: &ExpandAllHunkDiffs, cx: &mut ViewContext) { + self.display_map.update(cx, |display_map, cx| { + display_map.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + } + + pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext) { + let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + self.display_map.update(cx, |display_map, cx| { + if display_map.has_expanded_diff_hunks_in_ranges(&ranges, cx) { + display_map.collapse_diff_hunks(ranges, cx) + } else { + display_map.expand_diff_hunks(ranges, cx) + } + }) + } + pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut ViewContext) { if hovered != self.gutter_hovered { self.gutter_hovered = hovered; @@ -12233,7 +12250,12 @@ impl Editor { let buffer_id = buffer.read(cx).remote_id(); if !self.diff_map.diff_bases.contains_key(&buffer_id) { if let Some(project) = &self.project { - get_unstaged_changes_for_buffers(project, [buffer.clone()], cx); + get_unstaged_changes_for_buffers( + project, + [buffer.clone()], + self.display_map.clone(), + cx, + ); } } cx.emit(EditorEvent::ExcerptsAdded { @@ -12937,7 +12959,8 @@ impl Editor { fn get_unstaged_changes_for_buffers( project: &Model, buffers: impl IntoIterator>, - cx: &mut ViewContext, + display_map: Model, + cx: &mut AppContext, ) { let mut tasks = Vec::new(); project.update(cx, |project, cx| { @@ -12945,16 +12968,17 @@ fn get_unstaged_changes_for_buffers( tasks.push(project.open_unstaged_changes(buffer.clone(), cx)) } }); - cx.spawn(|this, mut cx| async move { + cx.spawn(|mut cx| async move { let change_sets = futures::future::join_all(tasks).await; - this.update(&mut cx, |this, cx| { - for change_set in change_sets { - if let Some(change_set) = change_set.log_err() { - this.diff_map.add_change_set(change_set, cx); + display_map + .update(&mut cx, |display_map, cx| { + for change_set in change_sets { + if let Some(change_set) = change_set.log_err() { + display_map.add_change_set(change_set, cx); + } } - } - }) - .ok(); + }) + .ok(); }) .detach(); } @@ -13242,56 +13266,6 @@ fn test_wrap_with_prefix() { ); } -fn hunks_for_selections( - snapshot: &EditorSnapshot, - selections: &[Selection], -) -> Vec { - hunks_for_ranges( - selections.iter().map(|selection| selection.range()), - snapshot, - ) -} - -pub fn hunks_for_ranges( - ranges: impl Iterator>, - snapshot: &EditorSnapshot, -) -> Vec { - let mut hunks = Vec::new(); - let mut processed_buffer_rows: HashMap>> = - HashMap::default(); - for query_range in ranges { - let query_rows = - MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1); - for hunk in snapshot.diff_map.diff_hunks_in_range( - Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0), - &snapshot.buffer_snapshot, - ) { - // Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it - // when the caret is just above or just below the deleted hunk. - let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed; - let related_to_selection = if allow_adjacent { - hunk.row_range.overlaps(&query_rows) - || hunk.row_range.start == query_rows.end - || hunk.row_range.end == query_rows.start - } else { - hunk.row_range.overlaps(&query_rows) - }; - if related_to_selection { - if !processed_buffer_rows - .entry(hunk.buffer_id) - .or_default() - .insert(hunk.buffer_range.start..hunk.buffer_range.end) - { - continue; - } - hunks.push(hunk); - } - } - } - - hunks -} - pub trait CollaborationHub { fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap; fn user_participant_indices<'a>( @@ -13840,6 +13814,45 @@ impl EditorSnapshot { }) } + pub fn hunks_for_ranges( + &self, + ranges: impl Iterator>, + ) -> Vec { + let mut hunks = Vec::new(); + let mut processed_buffer_rows: HashMap>> = + HashMap::default(); + for query_range in ranges { + let query_rows = + MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1); + for hunk in self.diff_snapshot().diff_hunks_in_range( + Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0), + ) { + // Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it + // when the caret is just above or just below the deleted hunk. + let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed; + let related_to_selection = if allow_adjacent { + hunk.row_range.overlaps(&query_rows) + || hunk.row_range.start == query_rows.end + || hunk.row_range.end == query_rows.start + } else { + hunk.row_range.overlaps(&query_rows) + }; + if related_to_selection { + if !processed_buffer_rows + .entry(hunk.buffer_id) + .or_default() + .insert(hunk.buffer_range.start..hunk.buffer_range.end) + { + continue; + } + hunks.push(hunk); + } + } + } + + hunks + } + pub fn language_at(&self, position: T) -> Option<&Arc> { self.display_snapshot.buffer_snapshot.language_at(position) } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 47bee57b58fa7..1cbdc1e75100f 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -3359,8 +3359,7 @@ async fn test_custom_newlines_cause_no_false_positive_diffs( let snapshot = editor.snapshot(cx); assert_eq!( snapshot - .diff_map - .diff_hunks_in_range(0..snapshot.buffer_snapshot.len(), &snapshot.buffer_snapshot) + .diff_hunks_in_range(0..snapshot.buffer_snapshot.len()) .collect::>(), Vec::new(), "Should not have any diffs for files with custom newlines" @@ -11655,7 +11654,9 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) { cx, ) }); - editor.diff_map.add_change_set(change_set, cx) + editor.display_map.update(cx, |display_map, cx| { + display_map.add_change_set(change_set, cx) + }); } }); cx.executor().run_until_parked(); @@ -12073,12 +12074,34 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test ); cx.update_editor(|editor, cx| { - for _ in 0..3 { + for _ in 0..2 { editor.go_to_next_hunk(&GoToHunk, cx); editor.toggle_hunk_diff(&ToggleHunkDiff, cx); } }); executor.run_until_parked(); + cx.assert_state_with_diff( + r#" + - use some::mod; + + ˇuse some::modified; + + + fn main() { + - println!("hello"); + + println!("hello there"); + + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + editor.go_to_next_hunk(&GoToHunk, cx); + editor.toggle_hunk_diff(&ToggleHunkDiff, cx); + }); + executor.run_until_parked(); cx.assert_state_with_diff( r#" - use some::mod; @@ -12164,7 +12187,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks( executor.run_until_parked(); cx.update_editor(|editor, cx| { - editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); cx.assert_state_with_diff( @@ -12209,7 +12232,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks( ); cx.update_editor(|editor, cx| { - editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); cx.assert_state_with_diff( @@ -12292,7 +12315,7 @@ async fn test_fold_unfold_diff_hunk(executor: BackgroundExecutor, cx: &mut gpui: executor.run_until_parked(); cx.update_editor(|editor, cx| { - editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); @@ -12485,7 +12508,9 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) cx, ) }); - editor.diff_map.add_change_set(change_set, cx) + editor.display_map.update(cx, |display_map, cx| { + display_map.add_change_set(change_set, cx) + }); } }) .unwrap(); @@ -12595,14 +12620,16 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext let buffer = buffer.read(cx).text_snapshot(); let change_set = cx .new_model(|cx| BufferChangeSet::new_with_base_text(base.to_string(), buffer, cx)); - editor.diff_map.add_change_set(change_set, cx) + editor.display_map.update(cx, |display_map, cx| { + display_map.add_change_set(change_set, cx) + }) }) .unwrap(); let mut cx = EditorTestContext::for_editor(editor, cx).await; cx.run_until_parked(); - cx.update_editor(|editor, cx| editor.expand_all_hunk_diffs(&Default::default(), cx)); + cx.update_editor(|editor, cx| editor.expand_all_diff_hunks(&Default::default(), cx)); cx.executor().run_until_parked(); cx.assert_state_with_diff( @@ -12666,7 +12693,7 @@ async fn test_edits_around_expanded_insertion_hunks( executor.run_until_parked(); cx.update_editor(|editor, cx| { - editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); @@ -12685,7 +12712,7 @@ async fn test_edits_around_expanded_insertion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -12708,7 +12735,7 @@ async fn test_edits_around_expanded_insertion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -12732,7 +12759,7 @@ async fn test_edits_around_expanded_insertion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -12757,7 +12784,7 @@ async fn test_edits_around_expanded_insertion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -12783,7 +12810,7 @@ async fn test_edits_around_expanded_insertion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -12804,7 +12831,7 @@ async fn test_edits_around_expanded_insertion_hunks( println!("world"); } - "# + "# .unindent(), ); } @@ -12857,7 +12884,7 @@ async fn test_edits_around_expanded_deletion_hunks( executor.run_until_parked(); cx.update_editor(|editor, cx| { - editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); @@ -12876,7 +12903,7 @@ async fn test_edits_around_expanded_deletion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -12899,7 +12926,7 @@ async fn test_edits_around_expanded_deletion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -12922,7 +12949,7 @@ async fn test_edits_around_expanded_deletion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -12946,7 +12973,7 @@ async fn test_edits_around_expanded_deletion_hunks( println!("world"); } - "# + "# .unindent(), ); } @@ -12999,7 +13026,7 @@ async fn test_edit_after_expanded_modification_hunk( cx.set_diff_base(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, cx| { - editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); @@ -14699,8 +14726,7 @@ fn assert_hunk_revert( let reverted_hunk_statuses = cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); let reverted_hunk_statuses = snapshot - .diff_map - .diff_hunks_in_range(0..snapshot.buffer_snapshot.len(), &snapshot.buffer_snapshot) + .diff_hunks_in_range(0..snapshot.buffer_snapshot.len()) .map(|hunk| hunk_status(&hunk)) .collect::>(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b75338c718fc0..3d7caf9926668 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -21,8 +21,8 @@ use crate::{ DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, InlineCompletion, JumpData, LineDown, - LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, - SoftWrap, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, + LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowInfo, RowRangeExt, SelectPhase, + Selection, SoftWrap, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, }; use client::ParticipantIndex; @@ -381,7 +381,7 @@ impl EditorElement { register_action(view, cx, Editor::toggle_git_blame); register_action(view, cx, Editor::toggle_git_blame_inline); register_action(view, cx, Editor::toggle_hunk_diff); - register_action(view, cx, Editor::expand_all_hunk_diffs); + register_action(view, cx, Editor::expand_all_diff_hunks); register_action(view, cx, |editor, action, cx| { if let Some(task) = editor.format(action, cx) { task.detach_and_log_err(cx); @@ -1196,7 +1196,7 @@ impl EditorElement { let editor = self.editor.read(cx); let is_singleton = editor.is_singleton(cx); // Git - (is_singleton && scrollbar_settings.git_diff && !snapshot.diff_map.is_empty()) + (is_singleton && scrollbar_settings.git_diff && snapshot.has_diff_hunks()) || // Buffer Search Results (is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::()) @@ -1461,10 +1461,10 @@ impl EditorElement { .unwrap_err(); let mut expanded_hunks = expanded_hunks[expanded_hunks_start_ix..].iter().peekable(); - let mut display_hunks: Vec<(DisplayDiffHunk, Option)> = editor - .diff_map - .snapshot - .diff_hunks_in_range(buffer_start..buffer_end, &buffer_snapshot) + let mut display_hunks: Vec<(DisplayDiffHunk, Option)> = snapshot + .display_snapshot + .diff_snapshot() + .diff_hunks_in_range(buffer_start..buffer_end) .filter_map(|hunk| { let display_hunk = diff_hunk_to_display(&hunk, snapshot); @@ -1532,7 +1532,7 @@ impl EditorElement { fn layout_inline_blame( &self, display_row: DisplayRow, - display_snapshot: &DisplaySnapshot, + row_info: &RowInfo, line_layout: &LineWithInvisibles, crease_trailer: Option<&CreaseTrailerLayout>, em_width: Pixels, @@ -1555,13 +1555,10 @@ impl EditorElement { .as_ref() .map(|(w, _)| w.clone()); - let display_point = DisplayPoint::new(display_row, 0); - let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row); - let blame = self.editor.read(cx).blame.clone()?; let blame_entry = blame .update(cx, |blame, cx| { - blame.blame_for_rows([Some(buffer_row)], cx).next() + blame.blame_for_rows(&[*row_info], cx).next() }) .flatten()?; @@ -1601,7 +1598,7 @@ impl EditorElement { #[allow(clippy::too_many_arguments)] fn layout_blame_entries( &self, - buffer_rows: impl Iterator>, + buffer_rows: &[RowInfo], em_width: Pixels, scroll_position: gpui::Point, line_height: Pixels, @@ -1938,7 +1935,7 @@ impl EditorElement { let end = rows.end.max(relative_to); let buffer_rows = snapshot - .buffer_rows(start) + .row_infos(start) .take(1 + end.minus(start) as usize) .collect::>(); @@ -1946,7 +1943,7 @@ impl EditorElement { let mut delta = 1; let mut i = head_idx + 1; while i < buffer_rows.len() as u32 { - if buffer_rows[i as usize].is_some() { + if buffer_rows[i as usize].buffer_row.is_some() { if rows.contains(&DisplayRow(i + start.0)) { relative_rows.insert(DisplayRow(i + start.0), delta); } @@ -1956,13 +1953,13 @@ impl EditorElement { } delta = 1; i = head_idx.min(buffer_rows.len() as u32 - 1); - while i > 0 && buffer_rows[i as usize].is_none() { + while i > 0 && buffer_rows[i as usize].buffer_row.is_none() { i -= 1; } while i > 0 { i -= 1; - if buffer_rows[i as usize].is_some() { + if buffer_rows[i as usize].buffer_row.is_some() { if rows.contains(&DisplayRow(i + start.0)) { relative_rows.insert(DisplayRow(i + start.0), delta); } @@ -1976,7 +1973,7 @@ impl EditorElement { fn layout_line_numbers( &self, rows: Range, - buffer_rows: impl Iterator>, + buffer_rows: &[RowInfo], active_rows: &BTreeMap, newest_selection_head: Option, snapshot: &EditorSnapshot, @@ -2018,8 +2015,7 @@ impl EditorElement { buffer_rows .into_iter() .enumerate() - .map(|(ix, multibuffer_row)| { - let multibuffer_row = multibuffer_row?; + .map(|(ix, row_info)| { let display_row = DisplayRow(rows.start.0 + ix as u32); let color = if active_rows.contains_key(&display_row) { cx.theme().colors().editor_active_line_number @@ -2027,7 +2023,7 @@ impl EditorElement { cx.theme().colors().editor_line_number }; line_number.clear(); - let default_number = multibuffer_row.0 + 1; + let default_number = row_info.buffer_row? + 1; let number = relative_rows .get(&DisplayRow(ix as u32 + rows.start.0)) .unwrap_or(&default_number); @@ -2052,7 +2048,7 @@ impl EditorElement { fn layout_crease_toggles( &self, rows: Range, - buffer_rows: impl IntoIterator>, + row_infos: &[RowInfo], active_rows: &BTreeMap, snapshot: &EditorSnapshot, cx: &mut WindowContext, @@ -2061,22 +2057,21 @@ impl EditorElement { && snapshot.mode == EditorMode::Full && self.editor.read(cx).is_singleton(cx); if include_fold_statuses { - buffer_rows + row_infos .into_iter() .enumerate() - .map(|(ix, row)| { - if let Some(multibuffer_row) = row { - let display_row = DisplayRow(rows.start.0 + ix as u32); - let active = active_rows.contains_key(&display_row); - snapshot.render_crease_toggle( - multibuffer_row, - active, - self.editor.clone(), - cx, - ) - } else { - None - } + .map(|(ix, info)| { + let row = info.buffer_row?; + let display_row = DisplayRow(rows.start.0 + ix as u32); + let active = active_rows.contains_key(&display_row); + + // todo(max): Retrieve the multibuffer row correctly + snapshot.render_crease_toggle( + MultiBufferRow(row), + active, + self.editor.clone(), + cx, + ) }) .collect() } else { @@ -2086,15 +2081,16 @@ impl EditorElement { fn layout_crease_trailers( &self, - buffer_rows: impl IntoIterator>, + buffer_rows: impl IntoIterator, snapshot: &EditorSnapshot, cx: &mut WindowContext, ) -> Vec> { buffer_rows .into_iter() - .map(|row| { - if let Some(multibuffer_row) = row { - snapshot.render_crease_trailer(multibuffer_row, cx) + .map(|row_info| { + // FIXME: These are not really MultiBufferRow?! + if let Some(row) = row_info.buffer_row { + snapshot.render_crease_trailer(MultiBufferRow(row), cx) } else { None } @@ -4376,32 +4372,29 @@ impl EditorElement { let max_point = snapshot.display_snapshot.buffer_snapshot.max_point(); let mut marker_quads = Vec::new(); if scrollbar_settings.git_diff { - let marker_row_ranges = snapshot - .diff_map - .diff_hunks(&snapshot.buffer_snapshot) - .map(|hunk| { - let start_display_row = - MultiBufferPoint::new(hunk.row_range.start.0, 0) - .to_display_point(&snapshot.display_snapshot) - .row(); - let mut end_display_row = - MultiBufferPoint::new(hunk.row_range.end.0, 0) - .to_display_point(&snapshot.display_snapshot) - .row(); - if end_display_row != start_display_row { - end_display_row.0 -= 1; - } - let color = match hunk_status(&hunk) { - DiffHunkStatus::Added => theme.status().created, - DiffHunkStatus::Modified => theme.status().modified, - DiffHunkStatus::Removed => theme.status().deleted, - }; - ColoredRange { - start: start_display_row, - end: end_display_row, - color, - } - }); + let marker_row_ranges = snapshot.diff_hunks().map(|hunk| { + let start_display_row = + MultiBufferPoint::new(hunk.row_range.start.0, 0) + .to_display_point(&snapshot.display_snapshot) + .row(); + let mut end_display_row = + MultiBufferPoint::new(hunk.row_range.end.0, 0) + .to_display_point(&snapshot.display_snapshot) + .row(); + if end_display_row != start_display_row { + end_display_row.0 -= 1; + } + let color = match hunk_status(&hunk) { + DiffHunkStatus::Added => theme.status().created, + DiffHunkStatus::Modified => theme.status().modified, + DiffHunkStatus::Removed => theme.status().deleted, + }; + ColoredRange { + start: start_display_row, + end: end_display_row, + color, + } + }); marker_quads.extend( scrollbar_layout @@ -5820,12 +5813,15 @@ impl Element for EditorElement { ); let end_row = DisplayRow(end_row); - let buffer_rows = snapshot - .buffer_rows(start_row) + let row_infos = snapshot + .row_infos(start_row) .take((start_row..end_row).len()) - .collect::>(); - let is_row_soft_wrapped = - |row| buffer_rows.get(row).copied().flatten().is_none(); + .collect::>(); + let is_row_soft_wrapped = |row: usize| { + row_infos + .get(row) + .map_or(true, |info| info.buffer_row.is_none()) + }; let start_anchor = if start_row == Default::default() { Anchor::min() @@ -5842,9 +5838,21 @@ impl Element for EditorElement { ) }; - let highlighted_rows = self + let mut highlighted_rows = self .editor .update(cx, |editor, cx| editor.highlighted_display_rows(cx)); + + for (ix, row_info) in row_infos.iter().enumerate() { + let color = match row_info.diff_status { + Some(DiffHunkStatus::Added) => style.status.created_background, + Some(DiffHunkStatus::Removed) => style.status.deleted_background, + _ => continue, + }; + highlighted_rows + .entry(start_row + DisplayRow(ix as u32)) + .or_insert(color); + } + let highlighted_ranges = self.editor.read(cx).background_highlights_in_range( start_anchor..end_anchor, &snapshot.display_snapshot, @@ -5884,7 +5892,7 @@ impl Element for EditorElement { let line_numbers = self.layout_line_numbers( start_row..end_row, - buffer_rows.iter().copied(), + &row_infos, &active_rows, newest_selection_head, &snapshot, @@ -5894,14 +5902,14 @@ impl Element for EditorElement { let mut crease_toggles = cx.with_element_namespace("crease_toggles", |cx| { self.layout_crease_toggles( start_row..end_row, - buffer_rows.iter().copied(), + &row_infos, &active_rows, &snapshot, cx, ) }); let crease_trailers = cx.with_element_namespace("crease_trailers", |cx| { - self.layout_crease_trailers(buffer_rows.iter().copied(), &snapshot, cx) + self.layout_crease_trailers(row_infos.iter().copied(), &snapshot, cx) }); let display_hunks = self.layout_gutter_git_hunks( @@ -6042,11 +6050,12 @@ impl Element for EditorElement { let display_row = newest_selection_head.row(); if (start_row..end_row).contains(&display_row) { let line_ix = display_row.minus(start_row) as usize; + let row_info = &row_infos[line_ix]; let line_layout = &line_layouts[line_ix]; let crease_trailer_layout = crease_trailers[line_ix].as_ref(); inline_blame = self.layout_inline_blame( display_row, - &snapshot.display_snapshot, + row_info, line_layout, crease_trailer_layout, em_width, @@ -6059,7 +6068,7 @@ impl Element for EditorElement { } let blamed_display_rows = self.layout_blame_entries( - buffer_rows.into_iter(), + &row_infos, em_width, scroll_position, line_height, @@ -6148,22 +6157,6 @@ impl Element for EditorElement { let gutter_settings = EditorSettings::get_global(cx).gutter; - let expanded_add_hunks_by_rows = self.editor.update(cx, |editor, _| { - editor - .diff_map - .hunks(false) - .filter(|hunk| hunk.status == DiffHunkStatus::Added) - .map(|expanded_hunk| { - let start_row = expanded_hunk - .hunk_range - .start - .to_display_point(&snapshot) - .row(); - (start_row, expanded_hunk.clone()) - }) - .collect::>() - }); - let rows_with_hunk_bounds = display_hunks .iter() .filter_map(|(hunk, hitbox)| Some((hunk, hitbox.as_ref()?.bounds))) @@ -6206,38 +6199,32 @@ impl Element for EditorElement { if show_code_actions { let newest_selection_point = newest_selection_head.to_point(&snapshot.display_snapshot); - let newest_selection_display_row = - newest_selection_point.to_display_point(&snapshot).row(); - if !expanded_add_hunks_by_rows - .contains_key(&newest_selection_display_row) + if !snapshot + .is_line_folded(MultiBufferRow(newest_selection_point.row)) { - if !snapshot - .is_line_folded(MultiBufferRow(newest_selection_point.row)) - { - let buffer = snapshot.buffer_snapshot.buffer_line_for_row( - MultiBufferRow(newest_selection_point.row), - ); - if let Some((buffer, range)) = buffer { - let buffer_id = buffer.remote_id(); - let row = range.start.row; - let has_test_indicator = self - .editor - .read(cx) - .tasks - .contains_key(&(buffer_id, row)); - - if !has_test_indicator { - code_actions_indicator = self - .layout_code_actions_indicator( - line_height, - newest_selection_head, - scroll_pixel_position, - &gutter_dimensions, - &gutter_hitbox, - &rows_with_hunk_bounds, - cx, - ); - } + let buffer = snapshot.buffer_snapshot.buffer_line_for_row( + MultiBufferRow(newest_selection_point.row), + ); + if let Some((buffer, range)) = buffer { + let buffer_id = buffer.remote_id(); + let row = range.start.row; + let has_test_indicator = self + .editor + .read(cx) + .tasks + .contains_key(&(buffer_id, row)); + + if !has_test_indicator { + code_actions_indicator = self + .layout_code_actions_indicator( + line_height, + newest_selection_head, + scroll_pixel_position, + &gutter_dimensions, + &gutter_hitbox, + &rows_with_hunk_bounds, + cx, + ); } } } @@ -7209,7 +7196,12 @@ mod tests { .update_window(*window, |_, cx| { element.layout_line_numbers( DisplayRow(0)..DisplayRow(6), - (0..6).map(MultiBufferRow).map(Some), + &(0..6) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), &Default::default(), Some(DisplayPoint::new(DisplayRow(0), 0)), &snapshot, diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index b4fe2efec6ac8..fad75cbb3a9e5 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -9,12 +9,13 @@ use git::{ use gpui::{Model, ModelContext, Subscription, Task}; use http_client::HttpClient; use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown}; -use multi_buffer::MultiBufferRow; use project::{Project, ProjectItem}; use smallvec::SmallVec; use sum_tree::SumTree; use url::Url; +use crate::RowInfo; + #[derive(Clone, Debug, Default)] pub struct GitBlameEntry { pub rows: u32, @@ -194,15 +195,15 @@ impl GitBlame { pub fn blame_for_rows<'a>( &'a mut self, - rows: impl 'a + IntoIterator>, + rows: &'a [RowInfo], cx: &mut ModelContext, ) -> impl 'a + Iterator> { self.sync(cx); let mut cursor = self.entries.cursor::(&()); - rows.into_iter().map(move |row| { - let row = row?; - cursor.seek_forward(&row.0, Bias::Right, &()); + rows.into_iter().map(move |info| { + let row = info.buffer_row?; + cursor.seek_forward(&row, Bias::Right, &()); cursor.item()?.blame.clone() }) } @@ -563,15 +564,38 @@ mod tests { use unindent::Unindent as _; use util::RandomCharIter; - macro_rules! assert_blame_rows { - ($blame:expr, $rows:expr, $expected:expr, $cx:expr) => { - assert_eq!( - $blame - .blame_for_rows($rows.map(MultiBufferRow).map(Some), $cx) - .collect::>(), - $expected - ); - }; + // macro_rules! assert_blame_rows { + // ($blame:expr, $rows:expr, $expected:expr, $cx:expr) => { + // assert_eq!( + // $blame + // .blame_for_rows($rows.map(MultiBufferRow).map(Some), $cx) + // .collect::>(), + // $expected + // ); + // }; + // } + + #[track_caller] + fn assert_blame_rows( + blame: &mut GitBlame, + rows: Range, + expected: Vec>, + cx: &mut ModelContext, + ) { + assert_eq!( + blame + .blame_for_rows( + &rows + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + cx + ) + .collect::>(), + expected + ); } fn init_test(cx: &mut gpui::TestAppContext) { @@ -634,7 +658,15 @@ mod tests { blame.update(cx, |blame, cx| { assert_eq!( blame - .blame_for_rows((0..1).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(0..1) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + cx + ) .collect::>(), vec![None] ); @@ -698,7 +730,15 @@ mod tests { // All lines assert_eq!( blame - .blame_for_rows((0..8).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(0..8) + .map(|buffer_row| RowInfo { + buffer_row: Some(buffer_row), + ..Default::default() + }) + .collect::>(), + cx + ) .collect::>(), vec![ Some(blame_entry("1b1b1b", 0..1)), @@ -714,7 +754,15 @@ mod tests { // Subset of lines assert_eq!( blame - .blame_for_rows((1..4).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(1..4) + .map(|buffer_row| RowInfo { + buffer_row: Some(buffer_row), + ..Default::default() + }) + .collect::>(), + cx + ) .collect::>(), vec![ Some(blame_entry("0d0d0d", 1..2)), @@ -725,7 +773,17 @@ mod tests { // Subset of lines, with some not displayed assert_eq!( blame - .blame_for_rows(vec![Some(MultiBufferRow(1)), None, None], cx) + .blame_for_rows( + &[ + RowInfo { + buffer_row: Some(1), + ..Default::default() + }, + Default::default(), + Default::default(), + ], + cx + ) .collect::>(), vec![Some(blame_entry("0d0d0d", 1..2)), None, None] ); @@ -777,16 +835,16 @@ mod tests { git_blame.update(cx, |blame, cx| { // Sanity check before edits: make sure that we get the same blame entry for all // lines. - assert_blame_rows!( + assert_blame_rows( blame, - (0..4), + 0..4, vec![ Some(blame_entry("1b1b1b", 0..4)), Some(blame_entry("1b1b1b", 0..4)), Some(blame_entry("1b1b1b", 0..4)), Some(blame_entry("1b1b1b", 0..4)), ], - cx + cx, ); }); @@ -795,11 +853,11 @@ mod tests { buffer.edit([(Point::new(0, 0)..Point::new(0, 0), "X")], None, cx); }); git_blame.update(cx, |blame, cx| { - assert_blame_rows!( + assert_blame_rows( blame, - (0..2), + 0..2, vec![None, Some(blame_entry("1b1b1b", 0..4))], - cx + cx, ); }); // Modify a single line, in the middle of the line @@ -807,21 +865,21 @@ mod tests { buffer.edit([(Point::new(1, 2)..Point::new(1, 2), "X")], None, cx); }); git_blame.update(cx, |blame, cx| { - assert_blame_rows!( + assert_blame_rows( blame, - (1..4), + 1..4, vec![ None, Some(blame_entry("1b1b1b", 0..4)), - Some(blame_entry("1b1b1b", 0..4)) + Some(blame_entry("1b1b1b", 0..4)), ], - cx + cx, ); }); // Before we insert a newline at the end, sanity check: git_blame.update(cx, |blame, cx| { - assert_blame_rows!(blame, (3..4), vec![Some(blame_entry("1b1b1b", 0..4))], cx); + assert_blame_rows(blame, 3..4, vec![Some(blame_entry("1b1b1b", 0..4))], cx); }); // Insert a newline at the end buffer.update(cx, |buffer, cx| { @@ -829,17 +887,17 @@ mod tests { }); // Only the new line is marked as edited: git_blame.update(cx, |blame, cx| { - assert_blame_rows!( + assert_blame_rows( blame, - (3..5), + 3..5, vec![Some(blame_entry("1b1b1b", 0..4)), None], - cx + cx, ); }); // Before we insert a newline at the start, sanity check: git_blame.update(cx, |blame, cx| { - assert_blame_rows!(blame, (2..3), vec![Some(blame_entry("1b1b1b", 0..4)),], cx); + assert_blame_rows(blame, 2..3, vec![Some(blame_entry("1b1b1b", 0..4))], cx); }); // Usage example @@ -849,11 +907,11 @@ mod tests { }); // Only the new line is marked as edited: git_blame.update(cx, |blame, cx| { - assert_blame_rows!( + assert_blame_rows( blame, - (2..4), - vec![None, Some(blame_entry("1b1b1b", 0..4)),], - cx + 2..4, + vec![None, Some(blame_entry("1b1b1b", 0..4))], + cx, ); }); } diff --git a/crates/editor/src/git/project_diff.rs b/crates/editor/src/git/project_diff.rs index e76e5922dbe9a..f8dce4368a1cf 100644 --- a/crates/editor/src/git/project_diff.rs +++ b/crates/editor/src/git/project_diff.rs @@ -150,7 +150,7 @@ impl ProjectDiffEditor { let editor = cx.new_view(|cx| { let mut diff_display_editor = Editor::for_multibuffer(excerpts.clone(), Some(project.clone()), true, cx); - diff_display_editor.set_expand_all_diff_hunks(); + diff_display_editor.set_expand_all_diff_hunks(cx); diff_display_editor }); @@ -311,9 +311,11 @@ impl ProjectDiffEditor { .update(&mut cx, |project_diff_editor, cx| { project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx); project_diff_editor.editor.update(cx, |editor, cx| { - for change_set in change_sets { - editor.diff_map.add_change_set(change_set, cx) - } + editor.display_map.update(cx, |display_map, cx| { + for change_set in change_sets { + display_map.add_change_set(change_set, cx) + } + }); }); }) .ok(); @@ -1193,9 +1195,9 @@ mod tests { cx, ) }); - file_a_editor - .diff_map - .add_change_set(change_set.clone(), cx); + file_a_editor.display_map.update(cx, |display_map, cx| { + display_map.add_change_set(change_set.clone(), cx) + }); project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { buffer_store.set_change_set( diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 76e7a92036517..159537e519780 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -6,11 +6,10 @@ use gpui::{ use language::{Buffer, BufferId, Point}; use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow, - MultiBufferSnapshot, ToOffset, ToPoint, + MultiBufferSnapshot, ToPoint, }; use project::buffer_store::BufferChangeSet; use std::{ops::Range, sync::Arc}; -use sum_tree::TreeMap; use text::OffsetRangeExt; use ui::{ prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement, @@ -20,10 +19,10 @@ use util::RangeExt; use workspace::Item; use crate::{ - editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyAllDiffHunks, - ApplyDiffHunk, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, - DisplayRow, DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, - RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff, + editor_settings::CurrentLineHighlight, hunk_status, ApplyAllDiffHunks, ApplyDiffHunk, + BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow, + DisplaySnapshot, Editor, EditorElement, GoToHunk, GoToPrevHunk, RevertFile, + RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff, }; #[derive(Debug, Clone)] @@ -37,7 +36,6 @@ pub(super) struct HoveredHunk { pub(super) struct DiffMap { pub(crate) hunks: Vec, pub(crate) diff_bases: HashMap, - pub(crate) snapshot: DiffMapSnapshot, hunk_update_tasks: HashMap, Task<()>>, expand_all: bool, } @@ -51,9 +49,6 @@ pub(super) struct ExpandedHunk { pub folded: bool, } -#[derive(Clone, Debug, Default)] -pub(crate) struct DiffMapSnapshot(TreeMap); - pub(crate) struct DiffBaseState { pub(crate) change_set: Model, pub(crate) last_version: Option, @@ -74,133 +69,7 @@ pub enum DisplayDiffHunk { }, } -impl DiffMap { - pub fn snapshot(&self) -> DiffMapSnapshot { - self.snapshot.clone() - } - - pub fn add_change_set( - &mut self, - change_set: Model, - cx: &mut ViewContext, - ) { - let buffer_id = change_set.read(cx).buffer_id; - self.snapshot - .0 - .insert(buffer_id, change_set.read(cx).diff_to_buffer.clone()); - self.diff_bases.insert( - buffer_id, - DiffBaseState { - last_version: None, - _subscription: cx.observe(&change_set, move |editor, change_set, cx| { - editor - .diff_map - .snapshot - .0 - .insert(buffer_id, change_set.read(cx).diff_to_buffer.clone()); - Editor::sync_expanded_diff_hunks(&mut editor.diff_map, buffer_id, cx); - }), - change_set, - }, - ); - Editor::sync_expanded_diff_hunks(self, buffer_id, cx); - } - - pub fn hunks(&self, include_folded: bool) -> impl Iterator { - self.hunks - .iter() - .filter(move |hunk| include_folded || !hunk.folded) - } -} - -impl DiffMapSnapshot { - pub fn is_empty(&self) -> bool { - self.0.values().all(|diff| diff.is_empty()) - } - - pub fn diff_hunks<'a>( - &'a self, - buffer_snapshot: &'a MultiBufferSnapshot, - ) -> impl Iterator + 'a { - self.diff_hunks_in_range(0..buffer_snapshot.len(), buffer_snapshot) - } - - pub fn diff_hunks_in_range<'a, T: ToOffset>( - &'a self, - range: Range, - buffer_snapshot: &'a MultiBufferSnapshot, - ) -> impl Iterator + 'a { - let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot); - buffer_snapshot - .excerpts_for_range(range.clone()) - .filter_map(move |excerpt| { - let buffer = excerpt.buffer(); - let buffer_id = buffer.remote_id(); - let diff = self.0.get(&buffer_id)?; - let buffer_range = excerpt.map_range_to_buffer(range.clone()); - let buffer_range = - buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end); - Some( - diff.hunks_intersecting_range(buffer_range, excerpt.buffer()) - .map(move |hunk| { - let start = - excerpt.map_point_from_buffer(Point::new(hunk.row_range.start, 0)); - let end = - excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0)); - MultiBufferDiffHunk { - row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row), - buffer_id, - buffer_range: hunk.buffer_range.clone(), - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - } - }), - ) - }) - .flatten() - } - - pub fn diff_hunks_in_range_rev<'a, T: ToOffset>( - &'a self, - range: Range, - buffer_snapshot: &'a MultiBufferSnapshot, - ) -> impl Iterator + 'a { - let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot); - buffer_snapshot - .excerpts_for_range_rev(range.clone()) - .filter_map(move |excerpt| { - let buffer = excerpt.buffer(); - let buffer_id = buffer.remote_id(); - let diff = self.0.get(&buffer_id)?; - let buffer_range = excerpt.map_range_to_buffer(range.clone()); - let buffer_range = - buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end); - Some( - diff.hunks_intersecting_range_rev(buffer_range, excerpt.buffer()) - .map(move |hunk| { - let start_row = excerpt - .map_point_from_buffer(Point::new(hunk.row_range.start, 0)) - .row; - let end_row = excerpt - .map_point_from_buffer(Point::new(hunk.row_range.end, 0)) - .row; - MultiBufferDiffHunk { - row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row), - buffer_id, - buffer_range: hunk.buffer_range.clone(), - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - } - }), - ) - }) - .flatten() - } -} - impl Editor { - pub fn set_expand_all_diff_hunks(&mut self) { - self.diff_map.expand_all = true; - } - pub(super) fn toggle_hovered_hunk( &mut self, hovered_hunk: &HoveredHunk, @@ -213,47 +82,6 @@ impl Editor { } } - pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext) { - let snapshot = self.snapshot(cx); - let selections = self.selections.all(cx); - self.toggle_hunks_expanded(hunks_for_selections(&snapshot, &selections), cx); - } - - pub fn expand_all_hunk_diffs(&mut self, _: &ExpandAllHunkDiffs, cx: &mut ViewContext) { - let snapshot = self.snapshot(cx); - let display_rows_with_expanded_hunks = self - .diff_map - .hunks(false) - .map(|hunk| &hunk.hunk_range) - .map(|anchor_range| { - ( - anchor_range - .start - .to_display_point(&snapshot.display_snapshot) - .row(), - anchor_range - .end - .to_display_point(&snapshot.display_snapshot) - .row(), - ) - }) - .collect::>(); - let hunks = self - .diff_map - .snapshot - .diff_hunks(&snapshot.display_snapshot.buffer_snapshot) - .filter(|hunk| { - let hunk_display_row_range = Point::new(hunk.row_range.start.0, 0) - .to_display_point(&snapshot.display_snapshot) - ..Point::new(hunk.row_range.end.0, 0) - .to_display_point(&snapshot.display_snapshot); - let row_range_end = - display_rows_with_expanded_hunks.get(&hunk_display_row_range.start.row()); - row_range_end.is_none() || row_range_end != Some(&hunk_display_row_range.end.row()) - }); - self.toggle_hunks_expanded(hunks.collect(), cx); - } - fn toggle_hunks_expanded( &mut self, hunks_to_toggle: Vec, @@ -497,7 +325,8 @@ impl Editor { cx: &mut ViewContext, ) { let snapshot = self.snapshot(cx); - let hunks = hunks_for_selections(&snapshot, &self.selections.all(cx)); + let ranges = self.selections.all(cx).into_iter().map(|s| s.range()); + let hunks = snapshot.hunks_for_ranges(ranges); let mut ranges_by_buffer = HashMap::default(); self.transact(cx, |editor, cx| { for hunk in hunks { @@ -521,10 +350,8 @@ impl Editor { } } - fn has_multiple_hunks(&self, cx: &AppContext) -> bool { - let snapshot = self.buffer.read(cx).snapshot(cx); - let mut hunks = self.diff_map.snapshot.diff_hunks(&snapshot); - hunks.nth(1).is_some() + fn has_multiple_hunks(&self, cx: &mut WindowContext) -> bool { + self.display_map.read(cx).has_multiple_hunks(cx) } fn hunk_header_block( @@ -865,23 +692,15 @@ impl Editor { } pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool { - if self.diff_map.expand_all { - return false; - } - self.diff_map.hunk_update_tasks.clear(); - self.clear_row_highlights::(); - let to_remove = self - .diff_map - .hunks - .drain(..) - .flat_map(|expanded_hunk| expanded_hunk.blocks.into_iter()) - .collect::>(); - if to_remove.is_empty() { - false - } else { - self.remove_blocks(to_remove, None, cx); - true - } + self.display_map.update(cx, |diff_map, cx| { + let ranges = vec![Anchor::min()..Anchor::max()]; + if diff_map.has_expanded_diff_hunks_in_ranges(&ranges, cx) { + diff_map.collapse_diff_hunks(ranges, cx); + true + } else { + false + } + }) } pub(super) fn sync_expanded_diff_hunks( @@ -909,8 +728,7 @@ impl Editor { .update(&mut cx, |editor, cx| { let snapshot = editor.snapshot(cx); let mut recalculated_hunks = snapshot - .diff_map - .diff_hunks(&snapshot.buffer_snapshot) + .diff_hunks() .filter(|hunk| hunk.buffer_id == buffer_id) .fuse() .peekable(); @@ -1418,7 +1236,9 @@ mod tests { cx, ) }); - editor.diff_map.add_change_set(change_set, cx) + editor.display_map.update(cx, |display_map, cx| { + display_map.add_change_set(change_set, cx) + }); } }) .unwrap(); @@ -1470,8 +1290,7 @@ mod tests { assert_eq!( snapshot - .diff_map - .diff_hunks_in_range(Point::zero()..Point::new(12, 0), &snapshot.buffer_snapshot) + .diff_hunks_in_range(Point::zero()..Point::new(12, 0)) .map(|hunk| (hunk_status(&hunk), hunk.row_range)) .collect::>(), &expected, @@ -1479,11 +1298,7 @@ mod tests { assert_eq!( snapshot - .diff_map - .diff_hunks_in_range_rev( - Point::zero()..Point::new(12, 0), - &snapshot.buffer_snapshot - ) + .diff_hunks_in_range_rev(Point::zero()..Point::new(12, 0)) .map(|hunk| (hunk_status(&hunk), hunk.row_range)) .collect::>(), expected diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index f4934c32b0abc..eb4b11cff388a 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -61,7 +61,7 @@ impl ProposedChangesEditor { let mut this = Self { editor: cx.new_view(|cx| { let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx); - editor.set_expand_all_diff_hunks(); + editor.set_expand_all_diff_hunks(cx); editor.set_completion_provider(None); editor.clear_code_action_providers(); editor.set_semantics_provider( @@ -223,9 +223,11 @@ impl ProposedChangesEditor { self.buffer_entries = buffer_entries; self.editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |selections| selections.refresh()); - for change_set in new_change_sets { - editor.diff_map.add_change_set(change_set, cx) - } + editor.display_map.update(cx, |display_map, cx| { + for change_set in new_change_sets { + display_map.add_change_set(change_set, cx) + } + }) }); } diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index aecdfa91c27eb..39d3eceb7b78e 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -1,6 +1,6 @@ use crate::{ - display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DiffRowHighlight, DisplayPoint, - Editor, MultiBuffer, RowExt, + display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, + RowExt, }; use collections::BTreeMap; use futures::Future; @@ -11,11 +11,12 @@ use gpui::{ }; use itertools::Itertools; use language::{Buffer, BufferSnapshot, LanguageRegistry}; -use multi_buffer::{ExcerptRange, ToPoint}; +use multi_buffer::ExcerptRange; use parking_lot::RwLock; use project::{FakeFs, Project}; use std::{ any::TypeId, + cmp, ops::{Deref, DerefMut, Range}, path::Path, sync::{ @@ -23,6 +24,7 @@ use std::{ Arc, }, }; +use text::Bias; use util::{ assert_set_eq, test::{generate_marked_text, marked_text_ranges}, @@ -332,83 +334,51 @@ impl EditorTestContext { /// /// Diff hunks are indicated by lines starting with `+` and `-`. #[track_caller] - pub fn assert_state_with_diff(&mut self, expected_diff: String) { - let has_diff_markers = expected_diff - .lines() - .any(|line| line.starts_with("+") || line.starts_with("-")); - let expected_diff_text = expected_diff - .split('\n') - .map(|line| { - let trimmed = line.trim(); - if trimmed.is_empty() { - String::new() - } else if has_diff_markers { - line.to_string() - } else { - format!(" {line}") - } + pub fn assert_state_with_diff(&mut self, expected_diff_text: String) { + let (snapshot, selections) = self + .editor + .update(&mut self.cx, |editor, cx| editor.selections.all_display(cx)); + + let diff_snapshot = snapshot.diff_snapshot(); + let diff_offsets = selections + .into_iter() + .map(|s| { + let start = snapshot.display_point_to_diff_offset(s.start, Bias::Left).0; + let end = snapshot.display_point_to_diff_offset(s.end, Bias::Left).0; + cmp::min(start, end)..cmp::max(start, end) }) - .join("\n"); + .collect::>(); - let actual_selections = self.editor_selections(); - let actual_marked_text = - generate_marked_text(&self.buffer_text(), &actual_selections, true); + let actual_marked_text = generate_marked_text(&diff_snapshot.text(), &diff_offsets, true); // Read the actual diff from the editor's row highlights and block // decorations. - let actual_diff = self.editor.update(&mut self.cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let insertions = editor - .highlighted_rows::() - .map(|(range, _)| { - let start = range.start.to_point(&snapshot.buffer_snapshot); - let end = range.end.to_point(&snapshot.buffer_snapshot); - start.row..end.row - }) - .collect::>(); - let deletions = editor - .diff_map - .hunks - .iter() - .filter_map(|hunk| { - if hunk.blocks.is_empty() { - return None; - } - let row = hunk - .hunk_range - .start - .to_point(&snapshot.buffer_snapshot) - .row; - let (_, buffer, _) = editor - .buffer() - .read(cx) - .excerpt_containing(hunk.hunk_range.start, cx) - .expect("no excerpt for expanded buffer's hunk start"); - let buffer_id = buffer.read(cx).remote_id(); - let change_set = &editor - .diff_map - .diff_bases - .get(&buffer_id) - .expect("should have a diff base for expanded hunk") - .change_set; - let deleted_text = change_set - .read(cx) - .base_text - .as_ref() - .expect("no base text for expanded hunk") - .read(cx) - .as_rope() - .slice(hunk.diff_base_byte_range.clone()) - .to_string(); - if let DiffHunkStatus::Modified | DiffHunkStatus::Removed = hunk.status { - Some((row, deleted_text)) - } else { - None + let line_infos = diff_snapshot.row_infos(0).collect::>(); + let has_diff = line_infos.iter().any(|info| info.diff_status.is_some()); + + let actual_diff = actual_marked_text + .split('\n') + .zip(line_infos) + .map(|(line, info)| { + let mut marker = match info.diff_status { + Some(DiffHunkStatus::Added) => "+ ", + Some(DiffHunkStatus::Removed) => "- ", + Some(DiffHunkStatus::Modified) => unreachable!(), + None => { + if has_diff { + " " + } else { + "" + } } - }) - .collect::>(); - format_diff(actual_marked_text, deletions, insertions) - }); + }; + if line.is_empty() { + marker = marker.trim(); + } + format!("{marker}{line}") + }) + .collect::>() + .join("\n"); pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state"); } @@ -503,46 +473,6 @@ impl EditorTestContext { } } -fn format_diff( - text: String, - actual_deletions: Vec<(u32, String)>, - actual_insertions: Vec>, -) -> String { - let mut diff = String::new(); - for (row, line) in text.split('\n').enumerate() { - let row = row as u32; - if row > 0 { - diff.push('\n'); - } - if let Some(text) = actual_deletions - .iter() - .find_map(|(deletion_row, deleted_text)| { - if *deletion_row == row { - Some(deleted_text) - } else { - None - } - }) - { - for line in text.lines() { - diff.push('-'); - if !line.is_empty() { - diff.push(' '); - diff.push_str(line); - } - diff.push('\n'); - } - } - let marker = if actual_insertions.iter().any(|range| range.contains(&row)) { - "+ " - } else { - " " - }; - diff.push_str(format!("{marker}{line}").trim_end()); - } - diff -} - impl Deref for EditorTestContext { type Target = gpui::VisualTestContext; diff --git a/crates/multi_buffer/Cargo.toml b/crates/multi_buffer/Cargo.toml index 444fe3c75c6cf..e3912e45eac1e 100644 --- a/crates/multi_buffer/Cargo.toml +++ b/crates/multi_buffer/Cargo.toml @@ -27,11 +27,13 @@ collections.workspace = true ctor.workspace = true env_logger.workspace = true futures.workspace = true +git.workspace = true gpui.workspace = true itertools.workspace = true language.workspace = true log.workspace = true parking_lot.workspace = true +project.workspace = true rand.workspace = true settings.workspace = true serde.workspace = true diff --git a/crates/multi_buffer/src/diff_map.rs b/crates/multi_buffer/src/diff_map.rs new file mode 100644 index 0000000000000..7dc09d643cf94 --- /dev/null +++ b/crates/multi_buffer/src/diff_map.rs @@ -0,0 +1,2563 @@ +use crate::MultiBufferChunks; + +use super::{ + Anchor, AnchorRangeExt, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow, MultiBufferRows, + MultiBufferSnapshot, ToOffset, ToPoint as _, +}; +use collections::HashMap; +use git::diff::DiffHunkStatus; +use gpui::{AppContext, Context as _, HighlightStyle, Model, ModelContext, Subscription}; +use language::{BufferChunks, BufferId, Chunk}; +use project::buffer_store::BufferChangeSet; +use std::{ + any::TypeId, + mem::{self}, + ops::Range, + sync::Arc, +}; +use sum_tree::{Cursor, SumTree, TreeMap}; +use text::{Bias, Edit, Patch, Point, TextSummary, ToOffset as _, ToPoint as _}; +use util::debug_panic; + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct RowInfo { + pub buffer_row: Option, + pub diff_status: Option, +} + +pub(crate) struct DiffMap { + snapshot: DiffMapSnapshot, + diff_bases: HashMap, + all_hunks_expanded: bool, + edits_since_sync: Patch, +} + +struct ChangeSetState { + change_set: Model, + _subscription: Subscription, +} + +#[derive(Clone)] +struct DiffSnapshot { + diff: git::diff::BufferDiff, + base_text: language::BufferSnapshot, + base_text_version: usize, +} + +#[derive(Clone)] +pub struct DiffMapSnapshot { + diffs: TreeMap, + pub(crate) transforms: SumTree, + pub(crate) version: usize, +} + +#[derive(Debug, Clone)] +pub(crate) enum DiffTransform { + BufferContent { + summary: TextSummary, + is_inserted_hunk: bool, + }, + DeletedHunk { + summary_including_newline: TextSummary, + buffer_id: BufferId, + base_text_byte_range: Range, + base_text_start: Point, + needs_newline: bool, + }, +} + +#[derive(Debug, Clone)] +pub(crate) struct DiffTransformSummary { + multibuffer_map: TextSummary, + diff_map: TextSummary, +} + +impl DiffTransformSummary { + pub fn multibuffer_point(&self) -> Point { + self.multibuffer_map.lines + } + pub fn multibuffer_offset(&self) -> usize { + self.multibuffer_map.len + } + pub fn diff_point(&self) -> DiffPoint { + DiffPoint(self.diff_map.lines) + } + pub fn diff_offset(&self) -> DiffOffset { + DiffOffset(self.diff_map.len) + } +} + +pub struct DiffMapChunks<'a> { + snapshot: &'a DiffMapSnapshot, + language_aware: bool, + cursor: Cursor<'a, DiffTransform, (DiffOffset, usize)>, + multibuffer_chunks: MultiBufferChunks<'a>, + multibuffer_chunk: Option>, + multibuffer_offset: usize, + offset: DiffOffset, + end_offset: DiffOffset, + diff_base_chunks: Option<(BufferId, BufferChunks<'a>)>, +} + +#[derive(Clone)] +pub struct DiffMapRows<'a> { + cursor: Cursor<'a, DiffTransform, (DiffPoint, Point)>, + diff_point: DiffPoint, + input_buffer_rows: MultiBufferRows<'a>, +} + +pub type DiffEdit = text::Edit; + +#[derive(Debug)] +enum ChangeKind { + DiffUpdated { base_changed: bool }, + InputEdited, + ExpandOrCollapseHunks { range: Range, expand: bool }, +} + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct DiffOffset(pub usize); + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct DiffPoint(pub Point); + +impl DiffPoint { + pub fn new(row: u32, col: u32) -> Self { + DiffPoint(Point::new(row, col)) + } + + pub fn row(&self) -> u32 { + self.0.row + } + pub fn column(&self) -> u32 { + self.0.column + } +} + +impl DiffMap { + pub fn new( + multibuffer_snapshot: &MultiBufferSnapshot, + cx: &mut AppContext, + ) -> (Model, DiffMapSnapshot) { + let snapshot = DiffMapSnapshot { + diffs: TreeMap::default(), + version: 0, + transforms: SumTree::from_item( + DiffTransform::BufferContent { + summary: multibuffer_snapshot.text_summary(), + is_inserted_hunk: false, + }, + &(), + ), + }; + + let this = cx.new_model(|_| Self { + snapshot: snapshot.clone(), + all_hunks_expanded: false, + diff_bases: HashMap::default(), + edits_since_sync: Patch::default(), + }); + + (this, snapshot) + } + + pub fn add_change_set( + &mut self, + change_set: Model, + cx: &mut ModelContext, + ) { + let buffer_id = change_set.read(cx).buffer_id; + self.buffer_diff_changed(change_set.clone(), cx); + self.diff_bases.insert( + buffer_id, + ChangeSetState { + _subscription: cx.observe(&change_set, Self::buffer_diff_changed), + change_set, + }, + ); + } + + pub fn diff_base_for(&self, buffer_id: BufferId) -> Option<&Model> { + self.diff_bases + .get(&buffer_id) + .map(|state| &state.change_set) + } + + pub fn sync( + &mut self, + multibuffer_snapshot: &MultiBufferSnapshot, + buffer_edits: Vec>, + cx: &mut ModelContext, + ) -> (DiffMapSnapshot, Vec) { + let changes = buffer_edits + .iter() + .map(|edit| (edit.clone(), ChangeKind::InputEdited)) + .collect::>(); + + self.snapshot.buffer = multibuffer_snapshot.clone(); + self.recompute_transforms(changes, multibuffer_snapshot, cx); + + ( + self.snapshot.clone(), + mem::take(&mut self.edits_since_sync).into_inner(), + ) + } + + fn buffer_diff_changed( + &mut self, + change_set: Model, + cx: &mut ModelContext, + ) { + let change_set = change_set.read(cx); + let buffer_id = change_set.buffer_id; + let diff = change_set.diff_to_buffer.clone(); + let base_text = change_set + .base_text + .as_ref() + .map(|buffer| buffer.read(cx).snapshot()); + let base_text_version_changed = self + .snapshot + .diffs + .get(&buffer_id) + .map_or(true, |snapshot| { + snapshot.base_text_version != change_set.base_text_version + }); + + if let Some(base_text) = base_text.clone() { + self.snapshot.diffs.insert( + buffer_id, + DiffSnapshot { + diff: diff.clone(), + base_text, + base_text_version: change_set.base_text_version, + }, + ); + } else { + self.snapshot.diffs.remove(&buffer_id); + } + + let multibuffer = self.multibuffer.read(cx); + let multibuffer_snapshot = self.snapshot.buffer(); + let changes = multibuffer + .ranges_for_buffer(buffer_id, cx) + .into_iter() + .map(|(_, range, _)| { + let multibuffer_start = multibuffer_snapshot.point_to_offset(range.start); + let multibuffer_end = multibuffer_snapshot.point_to_offset(range.end); + ( + text::Edit { + old: multibuffer_start..multibuffer_end, + new: multibuffer_start..multibuffer_end, + }, + ChangeKind::DiffUpdated { + base_changed: base_text_version_changed, + }, + ) + }) + .collect(); + self.recompute_transforms(changes, cx); + } + + pub(super) fn has_multiple_hunks(&self) -> bool { + self.snapshot + .diff_hunks_in_range(Anchor::min()..Anchor::max()) + .nth(1) + .is_some() + } + + pub(super) fn has_expanded_diff_hunks_in_ranges(&self, ranges: &[Range]) -> bool { + let mut cursor = self.snapshot.transforms.cursor::(&()); + let multibuffer_snapshot = self.snapshot.buffer(); + for range in ranges { + let range = range.to_point(multibuffer_snapshot); + let start = multibuffer_snapshot.point_to_offset(Point::new(range.start.row, 0)); + let end = multibuffer_snapshot.point_to_offset(Point::new(range.end.row + 1, 0)); + let start = start.saturating_sub(1); + let end = multibuffer_snapshot.len().min(end + 1); + cursor.seek(&start, Bias::Right, &()); + while *cursor.start() < end { + match cursor.item() { + Some(DiffTransform::DeletedHunk { .. }) + | Some(DiffTransform::BufferContent { + is_inserted_hunk: true, + .. + }) => return true, + _ => {} + } + cursor.next(&()); + } + } + false + } + + pub(super) fn expand_diff_hunks( + &mut self, + ranges: Vec>, + cx: &mut ModelContext, + ) { + self.expand_or_collapse_diff_hunks(ranges, true, cx); + } + + pub(super) fn collapse_diff_hunks( + &mut self, + ranges: Vec>, + cx: &mut ModelContext, + ) { + self.expand_or_collapse_diff_hunks(ranges, false, cx); + } + + pub(super) fn set_all_hunks_expanded(&mut self, cx: &mut ModelContext) { + self.all_hunks_expanded = true; + self.expand_or_collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], true, cx); + } + + fn expand_or_collapse_diff_hunks( + &mut self, + ranges: Vec>, + expand: bool, + cx: &mut ModelContext, + ) { + let multibuffer_snapshot = self.snapshot.buffer(); + let mut changes = Vec::new(); + for range in ranges.iter() { + let multibuffer_start = range.start.to_point(multibuffer_snapshot); + let multibuffer_end = range.end.to_point(multibuffer_snapshot); + let multibuffer_start = + multibuffer_snapshot.point_to_offset(Point::new(multibuffer_start.row, 0)); + let multibuffer_end = + multibuffer_snapshot.point_to_offset(Point::new(multibuffer_end.row + 1, 0)); + let expanded_start = multibuffer_start.saturating_sub(1); + let expanded_end = multibuffer_snapshot.len().min(multibuffer_end); + changes.push(( + text::Edit { + old: expanded_start..expanded_end, + new: expanded_start..expanded_end, + }, + ChangeKind::ExpandOrCollapseHunks { + range: multibuffer_start..multibuffer_end, + expand, + }, + )); + } + + self.recompute_transforms(changes, cx); + } + + fn recompute_transforms( + &mut self, + changes: Vec<(text::Edit, ChangeKind)>, + multibuffer_snapshot: &MultiBufferSnapshot, + cx: &mut ModelContext, + ) { + let multibuffer = self.multibuffer.read(cx); + let multibuffer_snapshot = self.snapshot.buffer(); + + let mut cursor = self.snapshot.transforms.cursor::<(usize, DiffOffset)>(&()); + let mut new_transforms = SumTree::default(); + let mut edits = Patch::default(); + + let mut changes = changes.into_iter().peekable(); + let mut delta = 0_isize; + while let Some((mut edit, mut operation)) = changes.next() { + let mut to_skip = cursor.slice(&edit.old.start, Bias::Left, &()); + while cursor.end(&()).0 < edit.old.start + || (cursor.end(&()).0 == edit.old.start && cursor.start().0 < edit.old.start) + { + to_skip.extend(cursor.item().cloned(), &()); + cursor.next(&()); + } + + self.append_transforms(&mut new_transforms, to_skip); + + let mut end_of_current_insert = 0; + loop { + let multibuffer_range = edit.new.clone(); + let edit_old_start = + cursor.start().1 + DiffOffset(edit.old.start - cursor.start().0); + let mut edit_old_end = + cursor.start().1 + DiffOffset(edit.old.end - cursor.start().0); + + for (buffer, buffer_range, excerpt_id) in + multibuffer.range_to_buffer_ranges(multibuffer_range.clone()) + { + let excerpt_range = multibuffer_snapshot + .range_for_excerpt::(excerpt_id) + .unwrap(); + let excerpt_buffer_range = multibuffer_snapshot + .buffer_range_for_excerpt(excerpt_id) + .unwrap(); + let buffer_id = buffer.remote_id(); + let diff_state = self.snapshot.diffs.get(&buffer_id); + + let buffer_anchor_range = buffer.anchor_before(buffer_range.start) + ..buffer.anchor_after(buffer_range.end); + let change_start_buffer_offset = buffer_range.start; + if let Some(diff_state) = diff_state { + let diff = &diff_state.diff; + let base_text = &diff_state.base_text; + + for hunk in diff.hunks_intersecting_range(buffer_anchor_range, buffer) { + let hunk_start_buffer_offset = + hunk.buffer_range.start.to_offset(buffer); + let hunk_end_buffer_offset = hunk.buffer_range.end.to_offset(buffer); + + let excerpt_buffer_range_start_offset = + excerpt_buffer_range.start.to_offset(buffer); + let hunk_start_multibuffer_offset = excerpt_range.start + + hunk_start_buffer_offset + .saturating_sub(excerpt_buffer_range_start_offset); + + self.push_buffer_content_transform( + &mut new_transforms, + hunk_start_multibuffer_offset, + end_of_current_insert, + ); + + while cursor.end(&()).0 < hunk_start_multibuffer_offset + || (cursor.end(&()).0 == hunk_start_multibuffer_offset + && cursor.start().0 < hunk_start_multibuffer_offset) + { + let Some(item) = cursor.item() else { + break; + }; + if let DiffTransform::DeletedHunk { .. } = item { + let old_range = cursor.start().1..cursor.end(&()).1; + let new_offset = + DiffOffset((old_range.start.0 as isize + delta) as usize); + delta -= (old_range.end - old_range.start).0 as isize; + let edit = Edit { + old: old_range, + new: new_offset..new_offset, + }; + edits.push(edit); + } + cursor.next(&()); + } + + let mut was_previously_expanded = false; + let mut previous_expanded_summary = TextSummary::default(); + if cursor.start().0 == hunk_start_multibuffer_offset { + match cursor.item() { + Some(DiffTransform::DeletedHunk { + summary_including_newline, + .. + }) => { + was_previously_expanded = true; + previous_expanded_summary = + summary_including_newline.clone(); + } + Some(DiffTransform::BufferContent { + is_inserted_hunk, .. + }) => { + was_previously_expanded = *is_inserted_hunk; + } + None => {} + }; + } + + let hunk_is_deletion = + hunk_start_buffer_offset == hunk_end_buffer_offset; + + let should_expand_hunk = match &operation { + ChangeKind::DiffUpdated { base_changed: true } => { + self.all_hunks_expanded + } + ChangeKind::ExpandOrCollapseHunks { range, expand } => { + let intersects = hunk_is_deletion + || (hunk_start_buffer_offset < range.end + && hunk_end_buffer_offset > range.start); + if *expand { + was_previously_expanded + || self.all_hunks_expanded + || intersects + } else { + !intersects + && (was_previously_expanded || self.all_hunks_expanded) + } + } + _ => was_previously_expanded || self.all_hunks_expanded, + }; + + if should_expand_hunk { + if hunk.diff_base_byte_range.len() > 0 + && hunk_start_buffer_offset >= change_start_buffer_offset + { + let mut text_cursor = base_text.as_rope().cursor(0); + let base_text_start = text_cursor + .summary::(hunk.diff_base_byte_range.start); + let mut base_text_summary = text_cursor + .summary::(hunk.diff_base_byte_range.end); + let mut needs_newline = false; + let mut diff_byte_range = + DiffOffset(hunk.diff_base_byte_range.len()); + if base_text_summary.last_line_chars > 0 { + diff_byte_range.0 += 1; + base_text_summary.add_newline(); + needs_newline = true; + } + + if !was_previously_expanded + || base_text_summary != previous_expanded_summary + { + let hunk_overshoot = + hunk_start_multibuffer_offset - cursor.start().0; + let old_start = + cursor.start().1 + DiffOffset(hunk_overshoot); + let old_end = + old_start + DiffOffset(previous_expanded_summary.len); + let new_start = + DiffOffset(new_transforms.summary().diff_map.len); + let new_end = new_start + diff_byte_range; + delta += diff_byte_range.0 as isize; + let edit = Edit { + old: old_start..old_end, + new: new_start..new_end, + }; + edits.push(edit); + } + + new_transforms.push( + DiffTransform::DeletedHunk { + base_text_byte_range: hunk.diff_base_byte_range.clone(), + summary_including_newline: base_text_summary, + buffer_id, + base_text_start, + needs_newline, + }, + &(), + ); + } + + if hunk_end_buffer_offset > hunk_start_buffer_offset { + let hunk_end_multibuffer_offset = excerpt_range.start + + hunk_end_buffer_offset + - excerpt_buffer_range_start_offset; + end_of_current_insert = hunk_end_multibuffer_offset; + } + + if was_previously_expanded { + cursor.next(&()); + } + } + } + } + } + + while cursor.end(&()).0 <= edit.old.end { + let Some(item) = cursor.item() else { + break; + }; + if let DiffTransform::DeletedHunk { .. } = item { + let old_range = cursor.start().1..cursor.end(&()).1; + let new_offset = DiffOffset((old_range.start.0 as isize + delta) as usize); + delta -= (old_range.end - old_range.start).0 as isize; + let edit = Edit { + old: old_range, + new: new_offset..new_offset, + }; + edits.push(edit); + } + + edit_old_end = cursor.start().1 + DiffOffset(edit.old.end - cursor.start().0); + + cursor.next(&()); + } + + self.push_buffer_content_transform( + &mut new_transforms, + edit.new.end, + end_of_current_insert, + ); + + if let ChangeKind::InputEdited = operation { + let edit_new_start = DiffOffset((edit_old_start.0 as isize + delta) as usize); + delta += (edit.new.end - edit.new.start) as isize + - (edit.old.end - edit.old.start) as isize; + let edit_new_end = DiffOffset((edit_old_end.0 as isize + delta) as usize); + let edit = DiffEdit { + old: edit_old_start..edit_old_end, + new: edit_new_start..edit_new_end, + }; + edits.push(edit); + } + + if let Some((next_edit, _)) = changes.peek() { + if next_edit.old.start < cursor.end(&()).0 { + (edit, operation) = changes.next().unwrap(); + continue; + } + } + + let suffix = cursor.end(&()).0 - edit.old.end; + let transform_end = new_transforms.summary().multibuffer_map.len + suffix; + self.push_buffer_content_transform( + &mut new_transforms, + transform_end, + end_of_current_insert, + ); + cursor.next(&()); + break; + } + } + + self.append_transforms(&mut new_transforms, cursor.suffix(&())); + self.edits_since_sync = self.edits_since_sync.compose(edits); + + drop(cursor); + self.snapshot.transforms = new_transforms; + self.snapshot.version += 1; + cx.notify(); + + #[cfg(test)] + self.check_invariants(); + } + + fn append_transforms( + &self, + new_transforms: &mut SumTree, + subtree: SumTree, + ) { + if let Some(DiffTransform::BufferContent { + is_inserted_hunk, + summary, + }) = subtree.first() + { + if self.extend_last_buffer_content_transform( + new_transforms, + *is_inserted_hunk, + summary.clone(), + ) { + let mut cursor = subtree.cursor::<()>(&()); + cursor.next(&()); + cursor.next(&()); + new_transforms.append(cursor.suffix(&()), &()); + return; + } + } + new_transforms.append(subtree, &()); + } + + fn push_buffer_content_transform( + &self, + new_transforms: &mut SumTree, + end_offset: usize, + end_of_current_inserted_hunk: usize, + ) { + for (end_offset, region_is_inserted_hunk) in [ + (end_offset.min(end_of_current_inserted_hunk), true), + (end_offset, false), + ] { + let start_offset = new_transforms.summary().multibuffer_map.len; + if end_offset <= start_offset { + continue; + } + let summary_to_add = self + .snapshot + .buffer + .text_summary_for_range::(start_offset..end_offset); + + if !self.extend_last_buffer_content_transform( + new_transforms, + region_is_inserted_hunk, + summary_to_add.clone(), + ) { + new_transforms.push( + DiffTransform::BufferContent { + summary: summary_to_add, + is_inserted_hunk: region_is_inserted_hunk, + }, + &(), + ) + } + } + } + + fn extend_last_buffer_content_transform( + &self, + new_transforms: &mut SumTree, + region_is_inserted_hunk: bool, + summary_to_add: TextSummary, + ) -> bool { + let mut did_extend = false; + new_transforms.update_last( + |last_transform| { + if let DiffTransform::BufferContent { + summary, + is_inserted_hunk, + } = last_transform + { + if *is_inserted_hunk == region_is_inserted_hunk { + *summary += summary_to_add.clone(); + did_extend = true; + } + } + }, + &(), + ); + did_extend + } + + #[cfg(test)] + fn check_invariants(&self) { + let snapshot = &self.snapshot; + if snapshot.transforms.summary().multibuffer_map.len != snapshot.buffer.len() { + panic!( + "incorrect input length. expected {}, got {}. transforms: {:+?}", + snapshot.buffer.len(), + snapshot.transforms.summary().multibuffer_map.len, + snapshot.transforms.items(&()), + ); + } + + let mut prev_transform: Option<&DiffTransform> = None; + for item in snapshot.transforms.iter() { + if let DiffTransform::BufferContent { + summary, + is_inserted_hunk, + } = item + { + if let Some(DiffTransform::BufferContent { + is_inserted_hunk: prev_is_inserted_hunk, + .. + }) = prev_transform + { + if *is_inserted_hunk == *prev_is_inserted_hunk { + panic!( + "multiple adjacent buffer content transforms with is_inserted_hunk = {is_inserted_hunk}. transforms: {:+?}", + snapshot.transforms.items(&())); + } + } + if summary.len == 0 && !snapshot.buffer().is_empty() { + panic!("empty buffer content transform"); + } + } + prev_transform = Some(item); + } + } +} + +impl DiffMapSnapshot { + pub fn diff_hunks_in_range<'a, T: ToOffset>( + &'a self, + multibuffer_snapshot: &'a MultiBufferSnapshot, + range: Range, + ) -> impl Iterator + 'a { + let range = + range.start.to_offset(multibuffer_snapshot)..range.end.to_offset(multibuffer_snapshot); + multibuffer_snapshot + .excerpts_for_range(range.clone()) + .filter_map(move |excerpt| { + let buffer = excerpt.buffer(); + let buffer_id = buffer.remote_id(); + let diff = &self.diffs.get(&buffer_id)?.diff; + let buffer_range = excerpt.map_range_to_buffer(range.clone()); + let buffer_range = + buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end); + Some( + diff.hunks_intersecting_range(buffer_range, excerpt.buffer()) + .map(move |hunk| { + let start = + excerpt.map_point_from_buffer(Point::new(hunk.row_range.start, 0)); + let end = + excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0)); + MultiBufferDiffHunk { + row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row), + buffer_id, + buffer_range: hunk.buffer_range.clone(), + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + } + }), + ) + }) + .flatten() + } + + pub fn diff_hunks_in_range_rev<'a, T: ToOffset>( + &'a self, + multibuffer_snapshot: &'a MultiBufferSnapshot, + range: Range, + ) -> impl Iterator + 'a { + let multibuffer_snapshot = self.buffer(); + let range = + range.start.to_offset(multibuffer_snapshot)..range.end.to_offset(multibuffer_snapshot); + multibuffer_snapshot + .excerpts_for_range_rev(range.clone()) + .filter_map(move |excerpt| { + let buffer = excerpt.buffer(); + let buffer_id = buffer.remote_id(); + let diff = &self.diffs.get(&buffer_id)?.diff; + let buffer_range = excerpt.map_range_to_buffer(range.clone()); + let buffer_range = + buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end); + Some( + diff.hunks_intersecting_range_rev(buffer_range, excerpt.buffer()) + .map(move |hunk| { + let start_row = excerpt + .map_point_from_buffer(Point::new(hunk.row_range.start, 0)) + .row; + let end_row = excerpt + .map_point_from_buffer(Point::new(hunk.row_range.end, 0)) + .row; + MultiBufferDiffHunk { + row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row), + buffer_id, + buffer_range: hunk.buffer_range.clone(), + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + } + }), + ) + }) + .flatten() + } + + pub fn has_diff_hunks(&self) -> bool { + self.diffs.values().any(|diff| !diff.diff.is_empty()) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn text(&self) -> String { + self.chunks(DiffOffset(0)..self.len(), false, None) + .map(|c| c.text) + .collect() + } + + pub fn len(&self) -> DiffOffset { + DiffOffset(self.transforms.summary().diff_map.len) + } + + pub fn max_point(&self) -> DiffPoint { + DiffPoint(self.transforms.summary().diff_map.lines) + } + + pub fn text_summary(&self) -> TextSummary { + self.transforms.summary().diff_map.clone() + } + + pub fn text_summary_for_range(&self, range: Range) -> TextSummary { + let mut cursor = self.transforms.cursor::<(DiffOffset, usize)>(&()); + cursor.seek(&range.start, Bias::Right, &()); + + let Some(first_transform) = cursor.item() else { + return TextSummary::default(); + }; + + let (diff_transform_start, multibuffer_transform_start) = cursor.start().clone(); + let (diff_transform_end, _) = cursor.end(&()); + let diff_start = range.start; + let diff_end = std::cmp::min(range.end, diff_transform_end); + + let mut result = match first_transform { + DiffTransform::BufferContent { .. } => { + let multibuffer_start = + multibuffer_transform_start + (diff_start - diff_transform_start).0; + let multibuffer_end = + multibuffer_transform_start + (diff_end - diff_transform_start).0; + + self.buffer + .text_summary_for_range(multibuffer_start..multibuffer_end) + } + DiffTransform::DeletedHunk { + buffer_id, + base_text_byte_range, + needs_newline, + .. + } => { + let buffer_start = + base_text_byte_range.start + (diff_start - diff_transform_start).0; + let mut buffer_end = + base_text_byte_range.start + (diff_end - diff_transform_start).0; + let Some(buffer_diff) = self.diffs.get(buffer_id) else { + panic!("{:?} is in non-extant deleted hunk", range.start) + }; + + if *needs_newline && diff_end == diff_transform_end { + buffer_end -= 1; + } + + let mut summary = buffer_diff + .base_text + .text_summary_for_range::(buffer_start..buffer_end); + + if *needs_newline && diff_end == diff_transform_end { + summary.add_newline(); + } + + summary + } + }; + if range.end < diff_transform_end { + return result; + } + + cursor.next(&()); + result = result + cursor.summary(&range.end, Bias::Right, &()); + + let Some(last_transform) = cursor.item() else { + return result; + }; + + let (diff_transform_start, multibuffer_transform_start) = cursor.start().clone(); + + result += match last_transform { + DiffTransform::BufferContent { .. } => { + let multibuffer_end = + multibuffer_transform_start + (range.end - diff_transform_start).0; + + self.buffer.text_summary_for_range::( + multibuffer_transform_start..multibuffer_end, + ) + } + DiffTransform::DeletedHunk { + base_text_byte_range, + buffer_id, + needs_newline, + .. + } => { + let buffer_end = base_text_byte_range.start + (range.end - diff_transform_start).0; + let Some(buffer_diff) = self.diffs.get(buffer_id) else { + panic!("{:?} is in non-extant deleted hunk", range.end) + }; + + let mut result = buffer_diff + .base_text + .text_summary_for_range::( + base_text_byte_range.start..buffer_end, + ); + + if *needs_newline && buffer_end == base_text_byte_range.end + 1 { + result.add_newline(); + } + + result + } + }; + + result + } + + pub fn buffer(&self) -> &MultiBufferSnapshot { + &self.buffer + } + + pub fn offset_to_point(&self, offset: DiffOffset) -> DiffPoint { + let mut cursor = self.transforms.cursor::(&()); + cursor.seek(&offset, Bias::Right, &()); + let start_transform = cursor.start(); + let overshoot = offset - start_transform.diff_offset(); + if overshoot.0 == 0 { + return start_transform.diff_point(); + } + + match cursor.item() { + Some(DiffTransform::BufferContent { .. }) => { + let multibuffer_offset = start_transform.multibuffer_offset() + overshoot.0; + let multibuffer_point = self.buffer.offset_to_point(multibuffer_offset); + start_transform.diff_point() + + DiffPoint(multibuffer_point - start_transform.multibuffer_point()) + } + Some(DiffTransform::DeletedHunk { + buffer_id, + base_text_start, + base_text_byte_range, + .. + }) => { + let Some(buffer_diff) = self.diffs.get(buffer_id) else { + panic!("{:?} is in non-extant deleted hunk", offset) + }; + let buffer_offset = base_text_byte_range.start + overshoot.0; + let buffer_point = buffer_diff.base_text.offset_to_point(buffer_offset); + start_transform.diff_point() + DiffPoint(buffer_point - base_text_start) + } + None => { + panic!("{:?} is past end of buffer", offset) + } + } + } + + pub fn clip_point(&self, point: DiffPoint, bias: Bias) -> DiffPoint { + let mut cursor = self.transforms.cursor::(&()); + cursor.seek(&point, Bias::Right, &()); + let start_transform = cursor.start(); + let overshoot = point - start_transform.diff_point(); + if overshoot.0.is_zero() { + return start_transform.diff_point(); + } + + match cursor.item() { + Some(DiffTransform::BufferContent { .. }) => { + let inlay_point = start_transform.multibuffer_point() + overshoot.0; + let clipped = self.buffer.clip_point(inlay_point, bias); + start_transform.diff_point() + + DiffPoint(clipped - start_transform.multibuffer_point()) + } + Some(DiffTransform::DeletedHunk { + buffer_id, + base_text_start, + .. + }) => { + let Some(buffer_diff) = self.diffs.get(buffer_id) else { + panic!("{:?} is in non-extant deleted hunk", point) + }; + let buffer_point = *base_text_start + overshoot.0; + let clipped = buffer_diff.base_text.clip_point(buffer_point, bias); + start_transform.diff_point() + DiffPoint(clipped - base_text_start) + } + None => cursor.end(&()).diff_point(), + } + } + + pub fn point_to_offset(&self, point: DiffPoint) -> DiffOffset { + let mut cursor = self.transforms.cursor::(&()); + cursor.seek(&point, Bias::Right, &()); + let start_transform = cursor.start(); + let overshoot = point - start_transform.diff_point(); + if overshoot.0.is_zero() { + return start_transform.diff_offset(); + } + + match cursor.item() { + Some(DiffTransform::BufferContent { .. }) => { + let multibuffer_point = start_transform.multibuffer_point() + overshoot.0; + let multibuffer_offset = self.buffer.point_to_offset(multibuffer_point); + start_transform.diff_offset() + + DiffOffset(multibuffer_offset - start_transform.multibuffer_offset()) + } + Some(DiffTransform::DeletedHunk { + buffer_id, + base_text_start, + base_text_byte_range, + .. + }) => { + let Some(buffer_diff) = self.diffs.get(buffer_id) else { + panic!("{:?} is in non-extant deleted hunk", point) + }; + let buffer_point = *base_text_start + overshoot.0; + let buffer_offset = buffer_diff.base_text.point_to_offset(buffer_point); + start_transform.diff_offset() + + DiffOffset(buffer_offset - base_text_byte_range.start) + } + None => { + panic!("{:?} is past end of buffer", point) + } + } + } + + // pub fn point_to_anchor(&self, point: DiffPoint, bias: Bias) -> DisplayAnchor { + // let mut cursor = self.transforms.cursor::<(DiffPoint, Point)>(&()); + // cursor.seek(&point, Bias::Right, &()); + // let (diff_start, multibuffer_start) = *cursor.start(); + // match cursor.item() { + // Some(DiffTransform::BufferContent { .. }) => { + // let multibuffer_point = multibuffer_start + (point - diff_start).0; + // DisplayAnchor { + // anchor: self.buffer.anchor_at(multibuffer_point, bias), + // diff_base_anchor: None, + // } + // } + // Some(DiffTransform::DeletedHunk { + // buffer_id, + // base_text_start, + // .. + // }) => { + // let diff_base_point = *base_text_start + (point - diff_start).0; + // let diff_base_anchor = if let Some(diff_base_snapshot) = self.diffs.get(&buffer_id) + // { + // Some( + // diff_base_snapshot + // .base_text + // .anchor_at(diff_base_point, bias), + // ) + // } else { + // debug_panic!("{} is missing diff base", buffer_id); + // None + // }; + + // DisplayAnchor { + // anchor: self.buffer.anchor_at(multibuffer_start, Bias::Left), + // diff_base_anchor, + // } + // } + // None => { + // panic!("{:?} is out of range", point) + // } + // } + // } + + // pub fn anchor_to_point(&self, anchor: DisplayAnchor) -> DiffPoint { + // let multibuffer_point = anchor.anchor.to_point(&self.buffer); + + // let mut cursor = self.transforms.cursor::<(Point, DiffPoint)>(&()); + // cursor.seek(&multibuffer_point, Bias::Left, &()); + + // if let Some(DiffTransform::DeletedHunk { + // buffer_id, + // base_text_start, + // .. + // }) = cursor.item() + // { + // if let Some(diff_base_anchor) = anchor.diff_base_anchor { + // if let Some(diff_base_snapshot) = self.diffs.get(&buffer_id) { + // if diff_base_anchor.buffer_id == Some(diff_base_snapshot.base_text.remote_id()) + // { + // let (_, diff_start) = *cursor.start(); + // let base_text_point = + // diff_base_anchor.to_point(&diff_base_snapshot.base_text); + // return diff_start + DiffPoint(base_text_point - base_text_start); + // } + // } + // } else { + // cursor.next(&()) + // } + // } + + // debug_assert!(matches!( + // cursor.item(), + // Some(DiffTransform::BufferContent { .. }) + // )); + + // let (multibuffer_start, diff_start) = *cursor.start(); + // diff_start + DiffPoint(multibuffer_point - multibuffer_start) + // } + + // pub fn compare_anchors(&self, a: &DisplayAnchor, b: &DisplayAnchor) -> std::cmp::Ordering { + // let buffer_cmp = a.anchor.cmp(&b.anchor, self.buffer()); + // if buffer_cmp != std::cmp::Ordering::Equal { + // return buffer_cmp; + // } + + // match (a.diff_base_anchor.is_some(), b.diff_base_anchor.is_some()) { + // (false, true) => return std::cmp::Ordering::Greater, + // (true, false) => return std::cmp::Ordering::Less, + // (false, false) => return std::cmp::Ordering::Equal, + // (true, true) => {} + // } + + // let diff_anchor_a = a.diff_base_anchor.unwrap(); + // let diff_anchor_b = b.diff_base_anchor.unwrap(); + + // if diff_anchor_a.buffer_id != diff_anchor_b.buffer_id || diff_anchor_a.buffer_id.is_none() { + // return std::cmp::Ordering::Equal; + // } + + // let Some(diff_base_snapshot) = self.diffs.get(&diff_anchor_a.buffer_id.unwrap()) else { + // return std::cmp::Ordering::Equal; + // }; + + // diff_anchor_a.cmp(&diff_anchor_b, &diff_base_snapshot.base_text) + // } + + pub fn to_multibuffer_offset(&self, offset: DiffOffset) -> usize { + let mut cursor = self.transforms.cursor::<(DiffOffset, usize)>(&()); + cursor.seek(&offset, Bias::Right, &()); + let mut multibuffer_offset = cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = cursor.item() { + let overshoot = offset.0 - cursor.start().0 .0; + multibuffer_offset += overshoot; + } + multibuffer_offset + } + + pub fn to_multibuffer_point(&self, point: DiffPoint) -> Point { + let mut cursor = self.transforms.cursor::<(DiffPoint, Point)>(&()); + cursor.seek(&point, Bias::Right, &()); + let mut inlay_point = cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = cursor.item() { + let overshoot = point.0 - cursor.start().0 .0; + inlay_point += overshoot; + } + inlay_point + } + + pub fn to_diff_offset(&self, multibuffer_offset: usize) -> DiffOffset { + let mut cursor = self.transforms.cursor::<(usize, DiffOffset)>(&()); + cursor.seek(&multibuffer_offset, Bias::Right, &()); + let mut diff_offset = cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = cursor.item() { + let overshoot = multibuffer_offset - cursor.start().0; + diff_offset.0 += overshoot; + } + diff_offset + } + + pub fn to_diff_point(&self, multibuffer_point: Point) -> DiffPoint { + let mut cursor = self.transforms.cursor::<(Point, DiffPoint)>(&()); + cursor.seek(&multibuffer_point, Bias::Right, &()); + let mut diff_point = cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = cursor.item() { + let overshoot = multibuffer_point - cursor.start().0; + diff_point.0 += overshoot; + } + diff_point + } + + pub(crate) fn chunks<'a>( + &'a self, + range: Range, + language_aware: bool, + text_highlights: Option<&'a TreeMap>)>>>, + ) -> DiffMapChunks<'a> { + let mut cursor = self.transforms.cursor::<(DiffOffset, usize)>(&()); + + cursor.seek(&range.end, Bias::Right, &()); + let mut multibuffer_end = cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = cursor.item() { + let overshoot = range.end.0 - cursor.start().0 .0; + multibuffer_end += overshoot; + } + + cursor.seek(&range.start, Bias::Right, &()); + let mut multibuffer_start = cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = cursor.item() { + let overshoot = range.start.0 - cursor.start().0 .0; + multibuffer_start += overshoot; + } + + let multibuffer_chunks = CustomHighlightsChunks::new( + multibuffer_start..multibuffer_end, + language_aware, + text_highlights, + &self.buffer, + ); + + DiffMapChunks { + snapshot: self, + language_aware, + cursor, + multibuffer_chunk: None, + multibuffer_chunks, + multibuffer_offset: multibuffer_start, + offset: range.start, + diff_base_chunks: None, + end_offset: range.end, + } + } + + pub fn row_infos(&self, start_row: u32) -> DiffMapRows { + if start_row > self.transforms.summary().diff_map.lines.row { + panic!("invalid diff map row {}", start_row); + } + + let diff_point = DiffPoint(Point::new(start_row, 0)); + let mut cursor = self.transforms.cursor::<(DiffPoint, Point)>(&()); + cursor.seek(&diff_point, Bias::Right, &()); + + let (diff_transform_start, buffer_transform_start) = cursor.start().clone(); + + let overshoot = if matches!(cursor.item(), Some(DiffTransform::BufferContent { .. })) { + diff_point.row() - diff_transform_start.row() + } else { + 0 + }; + let input_buffer_rows = self + .buffer + .buffer_rows(MultiBufferRow(buffer_transform_start.row + overshoot)); + + DiffMapRows { + diff_point, + input_buffer_rows, + cursor, + } + } +} + +impl<'a> DiffMapChunks<'a> { + pub fn seek(&mut self, range: Range) { + self.cursor.seek(&range.end, Bias::Right, &()); + let mut inlay_end = self.cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = self.cursor.item() { + let overshoot = range.end.0 - self.cursor.start().0 .0; + inlay_end += overshoot; + } + + self.cursor.seek(&range.start, Bias::Right, &()); + let mut inlay_start = self.cursor.start().1; + if let Some(DiffTransform::BufferContent { .. }) = self.cursor.item() { + let overshoot = range.start.0 - self.cursor.start().0 .0; + inlay_start += overshoot; + } + + self.multibuffer_chunks.seek(inlay_start..inlay_end); + self.multibuffer_chunk.take(); + self.multibuffer_offset = inlay_start; + self.offset = range.start; + self.end_offset = range.end; + } +} + +impl<'a> Iterator for DiffMapChunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + if self.offset >= self.end_offset { + return None; + } + if self.offset == self.cursor.end(&()).0 { + self.cursor.next(&()); + } + + let transform = self.cursor.item()?; + + match transform { + DiffTransform::BufferContent { summary, .. } => { + let chunk = self + .multibuffer_chunk + .get_or_insert_with(|| self.multibuffer_chunks.next().unwrap()); + + let chunk_end = self.offset + DiffOffset(chunk.text.len()); + let mut transform_end = self.cursor.start().0 + DiffOffset(summary.len); + + if transform_end > self.end_offset { + transform_end = self.end_offset + } + + if transform_end < chunk_end { + let (before, after) = chunk.text.split_at(transform_end.0 - self.offset.0); + self.offset = transform_end; + chunk.text = after; + Some(Chunk { + text: before, + ..chunk.clone() + }) + } else { + self.offset = chunk_end; + self.multibuffer_chunk.take() + } + } + DiffTransform::DeletedHunk { + buffer_id, + base_text_byte_range, + needs_newline, + .. + } => { + let hunk_start_offset = self.cursor.start().0; + let base_text_start_offset = base_text_byte_range.start; + let base_text_end_offset = base_text_byte_range.end; + let diff_base_end_offset = base_text_end_offset + .min(base_text_start_offset + self.end_offset.0 - hunk_start_offset.0); + let diff_base_start_offset = + base_text_start_offset + self.offset.0 - hunk_start_offset.0; + + let mut chunks = if let Some((_, mut chunks)) = self + .diff_base_chunks + .take() + .filter(|(id, _)| id == buffer_id) + { + if chunks.offset() != diff_base_start_offset { + chunks.seek(diff_base_start_offset..diff_base_end_offset); + } + chunks + } else { + let base_buffer = &self.snapshot.diffs.get(&buffer_id)?.base_text; + base_buffer.chunks( + diff_base_start_offset..diff_base_end_offset, + self.language_aware, + ) + }; + + let chunk = if let Some(chunk) = chunks.next() { + self.offset.0 += chunk.text.len(); + self.diff_base_chunks = Some((*buffer_id, chunks)); + chunk + } else { + debug_assert!(needs_newline); + self.offset.0 += "\n".len(); + Chunk { + text: "\n", + ..Default::default() + } + }; + Some(chunk) + } + } + } +} + +impl<'a> DiffMapRows<'a> { + pub fn seek(&mut self, row: u32) { + self.diff_point = DiffPoint::new(row, 0); + self.cursor.seek(&self.diff_point, Bias::Right, &()); + let (diff_transform_start, inlay_transform_start) = self.cursor.start().clone(); + let overshoot = if matches!( + self.cursor.item(), + Some(DiffTransform::BufferContent { .. }) + ) { + self.diff_point.row() - diff_transform_start.row() + } else { + 0 + }; + self.input_buffer_rows + .seek(MultiBufferRow(inlay_transform_start.row + overshoot)); + } +} + +impl<'a> Iterator for DiffMapRows<'a> { + type Item = RowInfo; + + fn next(&mut self) -> Option { + let result = match self.cursor.item() { + Some(DiffTransform::DeletedHunk { .. }) => Some(RowInfo { + buffer_row: None, + diff_status: Some(DiffHunkStatus::Removed), + }), + Some(DiffTransform::BufferContent { + is_inserted_hunk, .. + }) => { + let row = self.input_buffer_rows.next(); + row.map(|row| RowInfo { + buffer_row: row, + diff_status: if *is_inserted_hunk { + Some(DiffHunkStatus::Added) + } else { + None + }, + }) + } + None => self.input_buffer_rows.next().map(|row| RowInfo { + buffer_row: row, + diff_status: None, + }), + }; + self.diff_point.0 += Point::new(1, 0); + if self.diff_point >= self.cursor.end(&()).0 { + self.cursor.next(&()); + } + result + } +} + +impl sum_tree::Item for DiffTransform { + type Summary = DiffTransformSummary; + + fn summary(&self, _: &::Context) -> Self::Summary { + match self { + DiffTransform::BufferContent { summary, .. } => DiffTransformSummary { + multibuffer_map: summary.clone(), + diff_map: summary.clone(), + }, + DiffTransform::DeletedHunk { + summary_including_newline: summary, + .. + } => DiffTransformSummary { + multibuffer_map: TextSummary::default(), + diff_map: summary.clone(), + }, + } + } +} + +impl sum_tree::Summary for DiffTransformSummary { + type Context = (); + + fn zero(_: &Self::Context) -> Self { + DiffTransformSummary { + multibuffer_map: TextSummary::default(), + diff_map: TextSummary::default(), + } + } + + fn add_summary(&mut self, summary: &Self, _: &Self::Context) { + self.multibuffer_map += &summary.multibuffer_map; + self.diff_map += &summary.diff_map; + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for usize { + fn zero(_: &()) -> Self { + 0 + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + *self += summary.multibuffer_map.len + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for DiffOffset { + fn zero(_: &()) -> Self { + DiffOffset(0) + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + self.0 += summary.diff_map.len + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for Point { + fn zero(_: &()) -> Self { + Point::zero() + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + *self += summary.multibuffer_map.lines + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for DiffPoint { + fn zero(_: &()) -> Self { + DiffPoint(Point::zero()) + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + self.0 += summary.diff_map.lines + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for TextSummary { + fn zero(_cx: &()) -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + *self += &summary.diff_map; + } +} + +impl<'a> sum_tree::SeekTarget<'a, DiffTransformSummary, DiffTransformSummary> for DiffPoint { + fn cmp( + &self, + cursor_location: &DiffTransformSummary, + _: &::Context, + ) -> std::cmp::Ordering { + Ord::cmp(self, &cursor_location.diff_point()) + } +} + +impl<'a> sum_tree::SeekTarget<'a, DiffTransformSummary, DiffTransformSummary> for DiffOffset { + fn cmp( + &self, + cursor_location: &DiffTransformSummary, + _: &::Context, + ) -> std::cmp::Ordering { + Ord::cmp(self, &cursor_location.diff_offset()) + } +} + +impl std::ops::Add for DiffOffset { + type Output = DiffOffset; + + fn add(self, rhs: DiffOffset) -> Self::Output { + DiffOffset(self.0 + rhs.0) + } +} + +impl std::ops::AddAssign for DiffOffset { + fn add_assign(&mut self, rhs: DiffOffset) { + self.0 += rhs.0; + } +} + +impl std::ops::Sub for DiffOffset { + type Output = DiffOffset; + + fn sub(self, rhs: DiffOffset) -> Self::Output { + DiffOffset(self.0 - rhs.0) + } +} + +impl std::ops::SubAssign for DiffOffset { + fn sub_assign(&mut self, rhs: DiffOffset) { + self.0 -= rhs.0; + } +} + +impl std::ops::Add for DiffPoint { + type Output = DiffPoint; + + fn add(self, rhs: DiffPoint) -> Self::Output { + DiffPoint(self.0 + rhs.0) + } +} + +impl std::ops::AddAssign for DiffPoint { + fn add_assign(&mut self, rhs: DiffPoint) { + self.0 += rhs.0; + } +} + +impl std::ops::Sub for DiffPoint { + type Output = DiffPoint; + + fn sub(self, rhs: DiffPoint) -> Self::Output { + DiffPoint(self.0 - rhs.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::{AppContext, TestAppContext}; + use indoc::indoc; + use language::Buffer; + use multi_buffer::{Anchor, MultiBuffer}; + use project::Project; + use settings::SettingsStore; + use text::OffsetUtf16; + + #[gpui::test] + fn test_basic_diff_map_updates(cx: &mut TestAppContext) { + cx.update(init_test); + + let text = indoc!( + " + ZERO + one + TWO + three + six + " + ); + let base_text = indoc!( + " + one + two + three + four + five + six + " + ); + + let (diff_map, mut snapshot, mut deps) = build_diff_map(text, Some(base_text), cx); + assert_eq!( + snapshot.text(), + indoc!( + " + ZERO + one + TWO + three + six + " + ), + ); + + diff_map.update(cx, |diff_map, cx| { + diff_map.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + + ZERO + one + - two + + TWO + three + - four + - five + six + " + ), + ); + + assert_eq!( + snapshot + .row_infos(0) + .map(|info| info.buffer_row) + .collect::>(), + vec![ + Some(0), + Some(1), + None, + Some(2), + Some(3), + None, + None, + Some(4), + Some(5) + ] + ); + + assert_chunks_in_ranges(&snapshot); + assert_consistent_line_numbers(&snapshot); + + for (point, offset) in &[ + ( + DiffPoint::new(0, 0), + DiffOffset(snapshot.text().find("ZERO").unwrap()), + ), + ( + DiffPoint::new(2, 2), + DiffOffset(snapshot.text().find("two").unwrap() + 2), + ), + ( + DiffPoint::new(4, 3), + DiffOffset(snapshot.text().find("three").unwrap() + 3), + ), + (DiffPoint::new(8, 0), DiffOffset(snapshot.text().len())), + ] { + let actual = snapshot.point_to_offset(*point); + assert_eq!(actual, *offset, "for {:?}", point); + let actual = snapshot.offset_to_point(*offset); + assert_eq!(actual, *point, "for {:?}", offset); + } + + diff_map.update(cx, |diff_map, cx| { + diff_map.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + ZERO + one + TWO + three + six + " + ), + ); + + // Expand the first diff hunk + diff_map.update(cx, |diff_map, cx| { + let position = deps.multibuffer_snapshot.anchor_before(Point::new(2, 0)); + diff_map.expand_diff_hunks(vec![position..position], cx) + }); + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + ZERO + one + - two + + TWO + three + six + " + ), + ); + + // Expand the second diff hunk + diff_map.update(cx, |diff_map, cx| { + let position = deps.multibuffer_snapshot.anchor_before(Point::new(3, 0)); + diff_map.expand_diff_hunks(vec![position..position], cx) + }); + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + ZERO + one + - two + + TWO + three + - four + - five + six + " + ), + ); + + // Edit the buffer before the first hunk + let edits = deps.update_buffer(cx, |buffer, cx| { + buffer.edit_via_marked_text( + indoc!( + " + ZERO + one« hundred + thousand» + TWO + three + six + " + ), + None, + cx, + ); + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), edits, cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + ZERO + one hundred + thousand + - two + + TWO + three + - four + - five + six + " + ), + ); + + // Recalculate the diff, changing the first diff hunk. + let _ = deps.change_set.update(cx, |change_set, cx| { + change_set.recalculate_diff(deps.buffer.read(cx).text_snapshot(), cx) + }); + cx.run_until_parked(); + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), Vec::new(), cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + ZERO + one hundred + thousand + TWO + three + - four + - five + six + " + ), + ); + } + + #[gpui::test] + fn test_diff_map_multiple_buffer_edits(cx: &mut TestAppContext) { + cx.update(init_test); + + let text = "hello world"; + + let (diff_map, mut snapshot, mut deps) = build_diff_map(text, None, cx); + assert_eq!(snapshot.text(), "hello world"); + + let edits = deps.update_buffer(cx, |buffer, cx| { + buffer.edit([(4..5, "a"), (9..11, "k")], None, cx); + }); + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot, edits, cx) + }); + + assert_new_snapshot(&mut snapshot, sync, indoc!("hella work")); + } + + #[gpui::test] + fn test_diff_map_clipping(cx: &mut TestAppContext) { + cx.update(init_test); + + let text = indoc!( + "₀ + ₃ + ₄ + ₅ + " + ); + let base_text = indoc!( + "₀ + ₁ + ₂ + ₃ + ₄ + " + ); + + let (diff_map, mut diff_snapshot, deps) = build_diff_map(text, Some(base_text), cx); + diff_map.update(cx, |diff_map, cx| diff_map.set_all_hunks_expanded(cx)); + cx.run_until_parked(); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot, vec![], cx) + }); + + assert_new_snapshot( + &mut diff_snapshot, + sync, + indoc! {" + ₀ + - ₁ + - ₂ + ₃ + ₄ + + ₅ + "}, + ); + + for (point, (left, right)) in [ + ( + DiffPoint::new(0, 0), // start + (DiffPoint::new(0, 0), DiffPoint::new(0, 0)), + ), + ( + DiffPoint::new(1, 1), // deleted + (DiffPoint::new(1, 0), DiffPoint::new(1, 3)), + ), + ( + DiffPoint::new(3, 1), // unchanged + (DiffPoint::new(3, 0), DiffPoint::new(3, 3)), + ), + ( + DiffPoint::new(5, 2), // inserted + (DiffPoint::new(5, 0), DiffPoint::new(5, 3)), + ), + ( + DiffPoint::new(6, 0), // end + (DiffPoint::new(6, 0), DiffPoint::new(6, 0)), + ), + ( + DiffPoint::new(7, 7), // beyond + (DiffPoint::new(6, 0), DiffPoint::new(6, 0)), + ), + ] { + assert_eq!(left, diff_snapshot.clip_point(point, Bias::Left)); + assert_eq!(right, diff_snapshot.clip_point(point, Bias::Right)); + } + + assert_eq!( + diff_snapshot.text_summary_for_range(DiffOffset(0)..DiffOffset(0)), + TextSummary::default() + ); + assert_eq!( + diff_snapshot.text_summary_for_range(diff_snapshot.len()..diff_snapshot.len()), + TextSummary::default() + ); + let full_summary = TextSummary { + len: 24, + len_utf16: OffsetUtf16(12), + lines: Point { row: 6, column: 0 }, + first_line_chars: 1, + last_line_chars: 0, + last_line_len_utf16: 0, + longest_row: 0, + longest_row_chars: 1, + }; + let partial_summary = TextSummary { + len: 8, + len_utf16: OffsetUtf16(4), + lines: Point { row: 2, column: 0 }, + first_line_chars: 1, + last_line_chars: 0, + last_line_len_utf16: 0, + longest_row: 0, + longest_row_chars: 1, + }; + + let two = DiffOffset(diff_snapshot.text().find("₂").unwrap()); + let four = DiffOffset(diff_snapshot.text().find("₄").unwrap()); + + assert_eq!( + diff_snapshot.text_summary_for_range(DiffOffset(0)..diff_snapshot.len()), + full_summary + ); + assert_eq!( + diff_snapshot.text_summary_for_range(DiffOffset(0)..two), + partial_summary + ); + assert_eq!( + diff_snapshot.text_summary_for_range(two..four), + partial_summary + ); + assert_eq!( + diff_snapshot.text_summary_for_range(four..diff_snapshot.len()), + partial_summary + ); + } + + #[gpui::test] + fn test_empty_diff_map(cx: &mut TestAppContext) { + cx.update(init_test); + + let (_diff_map, diff_snapshot, _deps) = build_diff_map("", None, cx); + assert_eq!( + diff_snapshot + .row_infos(0) + .map(|info| info.buffer_row) + .collect::>(), + [Some(0)] + ); + } + + #[gpui::test] + fn test_expand_no_newline(cx: &mut TestAppContext) { + cx.update(init_test); + + let base_text = "todo"; + let text = indoc!( + " + one + two + " + ); + + let (diff_map, mut diff_snapshot, deps) = build_diff_map(text, Some(base_text), cx); + assert_eq!(diff_snapshot.text(), "one\ntwo\n"); + assert_eq!( + diff_snapshot + .row_infos(0) + .map(|info| info.buffer_row) + .collect::>(), + [Some(0), Some(1), Some(2)] + ); + + diff_map.update(cx, |diff_map, cx| { + diff_map.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut diff_snapshot, + sync, + indoc!( + " + - todo + + one + + two + " + ), + ); + + assert_eq!(diff_snapshot.text(), "todo\none\ntwo\n"); + + assert_eq!( + diff_snapshot + .row_infos(0) + .map(|info| info.buffer_row) + .collect::>(), + [None, Some(0), Some(1), Some(2)] + ); + assert_eq!( + diff_snapshot + .row_infos(1) + .map(|info| info.buffer_row) + .collect::>(), + [Some(0), Some(1), Some(2)] + ); + + for (point, offset) in [ + (DiffPoint::new(0, 4), DiffOffset(4)), + (DiffPoint::new(1, 0), DiffOffset(5)), + (DiffPoint::new(1, 1), DiffOffset(6)), + ] { + assert_eq!(diff_snapshot.offset_to_point(offset), point); + assert_eq!(diff_snapshot.point_to_offset(point), offset); + } + + for point in [ + DiffPoint::new(0, 4), + DiffPoint::new(1, 0), + DiffPoint::new(1, 1), + ] { + let anchor = diff_snapshot.point_to_anchor(point, Bias::Left); + dbg!(&anchor); + let actual = diff_snapshot.anchor_to_point(anchor); + assert_eq!(point, actual); + } + + assert_eq!( + diff_snapshot.clip_point(DiffPoint::new(0, 5), Bias::Left), + DiffPoint::new(0, 4) + ); + assert_eq!( + diff_snapshot.to_diff_point(Point::new(0, 0)), + DiffPoint::new(1, 0) + ); + assert_eq!( + diff_snapshot.to_multibuffer_point(DiffPoint::new(0, 4)), + Point::new(0, 0) + ); + } + + #[gpui::test] + fn test_expand_collapse_at_positions_adjacent_to_hunks(cx: &mut TestAppContext) { + cx.update(init_test); + + let base_text = indoc!( + " + one + two + three + four + five + six + seven + eight + " + ); + let text = indoc!( + " + one + two + five + six; seven + eight + " + ); + + let (diff_map, mut snapshot, deps) = build_diff_map(text, Some(base_text), cx); + + // Expand at the line right below a deleted hunk. + diff_map.update(cx, |diff_map, cx| { + let point = deps.multibuffer_snapshot.anchor_before(Point::new(2, 0)); + diff_map.expand_diff_hunks(vec![point..point], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + - three + - four + five + six; seven + eight + " + ), + ); + + // Collapse at the line right below a deleted hunk. + diff_map.update(cx, |diff_map, cx| { + let point = deps.multibuffer_snapshot.anchor_before(Point::new(2, 0)); + diff_map.collapse_diff_hunks(vec![point..point], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + five + six; seven + eight + " + ), + ); + + // Expand at the line right above a deleted hunk. + diff_map.update(cx, |diff_map, cx| { + let point = deps.multibuffer_snapshot.anchor_before(Point::new(1, 0)); + diff_map.expand_diff_hunks(vec![point..point], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + - three + - four + five + six; seven + eight + " + ), + ); + + eprintln!(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + + // Expand at the line right below a modified hunk. Should not expand anything. + diff_map.update(cx, |diff_map, cx| { + let point = deps.multibuffer_snapshot.anchor_before(Point::new(4, 0)); + diff_map.expand_diff_hunks(vec![point..point], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + - three + - four + five + six; seven + eight + " + ), + ); + } + + #[gpui::test] + fn test_expand_collapse_insertion_hunk(cx: &mut TestAppContext) { + cx.update(init_test); + + let base_text = indoc!( + " + one + two + seven + eight + " + ); + let text = indoc!( + " + one + two + three + four + five + six + seven + eight + " + ); + + let (diff_map, mut snapshot, deps) = build_diff_map(text, Some(base_text), cx); + + // Expand at the line right right after a deleted hunk. + diff_map.update(cx, |diff_map, cx| { + let point = deps.multibuffer_snapshot.anchor_before(Point::new(2, 0)); + diff_map.expand_diff_hunks(vec![point..point], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + + three + + four + + five + + six + seven + eight + " + ), + ); + + diff_map.update(cx, |diff_map, cx| { + let point = deps.multibuffer_snapshot.anchor_before(Point::new(2, 0)); + diff_map.collapse_diff_hunks(vec![point..point], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + three + four + five + six + seven + eight + " + ), + ); + } + + #[gpui::test] + fn test_edit_in_insertion_hunk(cx: &mut TestAppContext) { + cx.update(init_test); + + let base_text = indoc!( + " + one + two + six + seven + " + ); + let text = indoc!( + " + one + two + three + four + five + six + seven + " + ); + + let (diff_map, mut snapshot, mut deps) = build_diff_map(text, Some(base_text), cx); + + // Expand the hunk + diff_map.update(cx, |diff_map, cx| { + diff_map.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), vec![], cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + + three + + four + + five + six + seven + " + ), + ); + + eprintln!(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + + let edits = deps.update_buffer(cx, |buffer, cx| { + buffer.edit_via_marked_text( + indoc!( + " + one + two + three« + !» + four + five + six + seven + " + ), + None, + cx, + ) + }); + + let sync = diff_map.update(cx, |diff_map, cx| { + diff_map.sync(deps.multibuffer_snapshot.clone(), edits, cx) + }); + assert_new_snapshot( + &mut snapshot, + sync, + indoc!( + " + one + two + + three + + ! + + four + + five + six + seven + " + ), + ); + } + + // #[track_caller] + fn assert_new_snapshot( + snapshot: &mut DiffMapSnapshot, + (new_snapshot, edits): (DiffMapSnapshot, Vec>), + expected_diff: &str, + ) { + let actual_text = new_snapshot.text(); + let line_infos = new_snapshot.row_infos(0).collect::>(); + let has_diff = line_infos.iter().any(|info| info.diff_status.is_some()); + let actual_diff = actual_text + .split('\n') + .zip(line_infos) + .map(|(line, info)| { + let marker = match info.diff_status { + Some(DiffHunkStatus::Added) => "+ ", + Some(DiffHunkStatus::Removed) => "- ", + Some(DiffHunkStatus::Modified) => unreachable!(), + None => { + if has_diff { + " " + } else { + "" + } + } + }; + if line.is_empty() { + String::new() + } else { + format!("{marker}{line}") + } + }) + .collect::>() + .join("\n"); + pretty_assertions::assert_eq!(actual_diff, expected_diff); + check_edits(snapshot, &new_snapshot, &edits); + *snapshot = new_snapshot; + } + + #[track_caller] + fn check_edits( + old_snapshot: &DiffMapSnapshot, + new_snapshot: &DiffMapSnapshot, + edits: &[DiffEdit], + ) { + let mut text = old_snapshot.text(); + let new_text = new_snapshot.text(); + for edit in edits.iter().rev() { + if !text.is_char_boundary(edit.old.start.0) + || !text.is_char_boundary(edit.old.end.0) + || !new_text.is_char_boundary(edit.new.start.0) + || !new_text.is_char_boundary(edit.new.end.0) + { + panic!( + "invalid edits: {:?}\nold text: {:?}\nnew text: {:?}", + edits, text, new_text + ); + } + + text.replace_range( + edit.old.start.0..edit.old.end.0, + &new_text[edit.new.start.0..edit.new.end.0], + ); + } + + pretty_assertions::assert_eq!(text, new_text, "invalid edits: {:?}", edits); + } + + #[track_caller] + fn assert_chunks_in_ranges(snapshot: &DiffMapSnapshot) { + let full_text = snapshot.text(); + for ix in 0..full_text.len() { + let offset = DiffOffset(ix); + let mut chunks = + snapshot.chunks(DiffOffset(0)..snapshot.len(), false, Default::default()); + chunks.seek(offset..snapshot.len()); + let tail = chunks.map(|chunk| chunk.text).collect::(); + assert_eq!(tail, &full_text[ix..], "seek to range: {:?}", ix..); + + let tail = snapshot + .chunks(offset..snapshot.len(), false, Default::default()) + .map(|chunk| chunk.text) + .collect::(); + assert_eq!(tail, &full_text[ix..], "start from range: {:?}", ix..); + + let head = snapshot + .chunks(DiffOffset(0)..offset, false, Default::default()) + .map(|chunk| chunk.text) + .collect::(); + assert_eq!(head, &full_text[..ix], "start with range: {:?}", ..ix); + } + } + + #[track_caller] + fn assert_consistent_line_numbers(snapshot: &DiffMapSnapshot) { + let all_line_numbers = snapshot.row_infos(0).collect::>(); + for start_row in 1..all_line_numbers.len() { + let line_numbers = snapshot.row_infos(start_row as u32).collect::>(); + assert_eq!( + line_numbers, + all_line_numbers[start_row..], + "start_row: {start_row}" + ); + + for seek_row in 0..all_line_numbers.len() { + let mut numbers = snapshot.row_infos(start_row as u32); + numbers.seek(seek_row as u32); + let line_numbers = numbers.collect::>(); + assert_eq!( + line_numbers, + all_line_numbers[seek_row..], + "seek_row: {seek_row}, start_row: {start_row}" + ); + } + } + } + + struct DiffMapDeps { + buffer: Model, + multibuffer: Model, + change_set: Model, + multibuffer_snapshot: MultiBufferSnapshot, + multibuffer_edits: text::Subscription, + } + + fn build_diff_map( + text: &str, + base_text: Option<&str>, + cx: &mut TestAppContext, + ) -> (Model, DiffMapSnapshot, DiffMapDeps) { + let buffer = cx.new_model(|cx| Buffer::local(text, cx)); + + let change_set = cx.new_model(|cx| { + let text_snapshot = buffer.read(cx).text_snapshot(); + let mut change_set = BufferChangeSet::new(&text_snapshot); + if let Some(base_text) = base_text { + let _ = change_set.set_base_text(base_text.to_string(), text_snapshot, cx); + } + change_set + }); + + let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); + + let (multibuffer_snapshot, multibuffer_edits) = + multibuffer.update(cx, |buffer, cx| (buffer.snapshot(cx), buffer.subscribe())); + + let (diff_map, diff_map_snapshot) = cx.update(|cx| DiffMap::new(multibuffer.clone(), cx)); + diff_map.update(cx, |diff_map, cx| { + diff_map.add_change_set(change_set.clone(), cx) + }); + cx.run_until_parked(); + + ( + diff_map, + diff_map_snapshot, + DiffMapDeps { + buffer, + multibuffer, + change_set, + multibuffer_snapshot, + multibuffer_edits, + }, + ) + } + + impl DiffMapDeps { + fn update_buffer( + &mut self, + cx: &mut TestAppContext, + f: impl FnOnce(&mut Buffer, &mut ModelContext), + ) -> Vec> { + self.buffer.update(cx, f); + + self.multibuffer_snapshot = self + .multibuffer + .read_with(cx, |buffer, cx| buffer.snapshot(cx)); + self.multibuffer_edits.consume().into_inner() + } + } + + fn init_test(cx: &mut AppContext) { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + language::init(cx); + crate::init(cx); + Project::init_settings(cx); + } +} diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 0e688e6cdd2bf..3f8d8f87df34b 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1,9 +1,11 @@ mod anchor; +mod diff_map; pub use anchor::{Anchor, AnchorRangeExt, Offset}; use anyhow::{anyhow, Result}; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; +pub use diff_map::*; use futures::{channel::mpsc, SinkExt}; use gpui::{AppContext, EntityId, EventEmitter, Model, ModelContext, Task}; use itertools::Itertools; @@ -69,6 +71,7 @@ pub struct MultiBuffer { history: History, title: Option, capability: Capability, + diff_map: Model, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -179,6 +182,7 @@ struct BufferState { pub struct MultiBufferSnapshot { singleton: bool, excerpts: SumTree, + // diff_map_snapshot: DiffMapSnapshot, excerpt_ids: SumTree, trailing_excerpt_update_count: usize, non_text_state_update_count: usize, @@ -1547,6 +1551,36 @@ impl MultiBuffer { excerpts } + pub fn ranges_for_buffer( + &self, + buffer_id: BufferId, + cx: &AppContext, + ) -> Vec<(ExcerptId, Range, Range)> { + let mut ranges = Vec::new(); + let snapshot = self.read(cx); + for locator in self + .buffers + .borrow() + .get(&buffer_id) + .map(|state| &state.excerpts) + .into_iter() + .flatten() + { + let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, Point)>(&()); + cursor.seek_forward(&Some(locator), Bias::Left, &()); + if let Some(excerpt) = cursor.item() { + if excerpt.locator == *locator { + ranges.push(( + excerpt.id, + cursor.start().1..cursor.end(&()).1, + excerpt.range.context.clone(), + )); + } + } + } + ranges + } + pub fn excerpt_ranges_for_buffer( &self, buffer_id: BufferId, @@ -2529,6 +2563,39 @@ impl MultiBufferSnapshot { .eq(needle.bytes()) } + pub fn range_to_buffer_ranges( + &self, + range: Range, + ) -> Vec<(BufferSnapshot, Range, ExcerptId)> { + let start = range.start.to_offset(&self); + let end = range.end.to_offset(&self); + + let mut result = Vec::new(); + let mut cursor = self.excerpts.cursor::(&()); + cursor.seek(&start, Bias::Right, &()); + if cursor.item().is_none() { + cursor.prev(&()); + } + + while let Some(excerpt) = cursor.item() { + if *cursor.start() > end { + break; + } + + let mut end_before_newline = cursor.end(&()); + if excerpt.has_trailing_newline { + end_before_newline -= 1; + } + let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start()); + let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start()); + result.push((excerpt.buffer.clone(), start..end, excerpt.id)); + cursor.next(&()); + } + + result + } + pub fn surrounding_word( &self, start: T, @@ -4080,6 +4147,17 @@ impl MultiBufferSnapshot { } } + pub fn buffer_range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option> { + let mut cursor = self.excerpts.cursor::>(&()); + let locator = self.excerpt_locator_for_id(excerpt_id); + if cursor.seek(&Some(locator), Bias::Left, &()) { + if let Some(excerpt) = cursor.item() { + return Some(excerpt.range.context.clone()); + } + } + None + } + fn excerpt(&self, excerpt_id: ExcerptId) -> Option<&Excerpt> { let mut cursor = self.excerpts.cursor::>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); @@ -4675,6 +4753,10 @@ impl<'a> MultiBufferExcerpt<'a> { self.excerpt_position } + pub fn end_point(&self) -> Point { + self.excerpt_position + self.excerpt.text_summary.lines + } + /// Maps an offset within the [`MultiBuffer`] to an offset within the [`Buffer`] pub fn map_offset_to_buffer(&self, offset: usize) -> usize { self.excerpt.buffer_start_offset() diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 89cb1e7b6319f..4e3323019095b 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -989,6 +989,14 @@ impl TextSummary { column: self.last_line_len_utf16, } } + + pub fn add_newline(&mut self) { + self.len += 1; + self.len_utf16 += OffsetUtf16(self.len_utf16.0 + 1); + self.last_line_chars = 0; + self.last_line_len_utf16 = 0; + self.lines += Point::new(1, 0); + } } impl<'a> From<&'a str> for TextSummary { diff --git a/crates/text/src/patch.rs b/crates/text/src/patch.rs index bb71a80c5128b..ff1d0c2e9ab09 100644 --- a/crates/text/src/patch.rs +++ b/crates/text/src/patch.rs @@ -42,6 +42,7 @@ where self.0 } + #[must_use] pub fn compose(&self, new_edits_iter: impl IntoIterator>) -> Self { let mut old_edits_iter = self.0.iter().cloned().peekable(); let mut new_edits_iter = new_edits_iter.into_iter().peekable();