diff --git a/Cargo.lock b/Cargo.lock index a7f690078f21ad..83472cf28317d6 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/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 58d1b2f4f06ce8..87ea4425e5bd24 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -56,7 +56,7 @@ use language::{ use lsp::DiagnosticSeverity; use multi_buffer::{ Anchor, AnchorRangeExt, MultiBuffer, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, - MultiBufferSnapshot, ToOffset, ToPoint, + MultiBufferSnapshot, RowInfo, ToOffset, ToPoint, }; use project::buffer_store::BufferChangeSet; use serde::Deserialize; @@ -82,12 +82,6 @@ pub enum FoldStatus { Foldable, } -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub struct RowInfo { - pub buffer_row: Option, - pub diff_status: Option, -} - pub type RenderFoldToggle = Arc AnyElement>; pub trait ToDisplayPoint { diff --git a/crates/multi_buffer/Cargo.toml b/crates/multi_buffer/Cargo.toml index 444fe3c75c6cfb..e3912e45eac1e9 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 00000000000000..7dc09d643cf94a --- /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 f840dc4357c0c2..3f8d8f87df34ba 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, @@ -2559,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,