git: Refactor buffer_diff point translation APIs for more efficient side-by-side diff syncing (#48237)

The side-by-side diff heavily relies on a primitive from `buffer_diff`
that converts a point on one side of the diff to a range of points on
the other side. The way this primitive is set up on main is pretty
naive--every time we call `points_to_base_text_points` (or
`base_text_points_to_points`), we need to iterate over all hunks in the
diff. That's particularly bad for the case of constructing a new
side-by-side diff starting from a multibuffer, because we call those
APIs once per excerpt, and the number of excerpts is ~equal to the
number of hunks.

This PR changes the point translation APIs exposed by `buffer_diff` to
make it easier to use them efficiently in `editor`. The new shape is a
pair of functions that return a patch that can be used to translate from
the main buffer to the base text or vice versa. When syncing edits
through the block map that touch several excerpts for the same buffer,
we can reuse this patch for excerpts after the first--so when building a
new side-by-side diff, we'll iterate over each hunk just once.

The shape of the new APIs also sets us up to scale down to cases like
editing on the right-hand side of the diff: we can pass in a point range
and give them permission to return an approximate patch that's only
guaranteed to give the correct results when used with points in that
range. For edits that only affect one excerpt, and given how the project
diff is set up, that should allow us to skip iterating over most of the
hunks in a buffer.

Release Notes:

- N/A

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
This commit is contained in:
Cole Millerandcameron authored and GitHub committed 2026-02-03 22:18:59 -05:00
1 parent 0d99eb5d40
commit 6d2ed6c3f0
6 files changed
+348 -1615

No files matched your search

File diff suppressed because it is too large. Load diff
+24 -12
View File
@@ -150,12 +150,11 @@ type TextHighlights = TreeMap<HighlightKey, Arc<(HighlightStyle, Vec<Range<Ancho
type InlayHighlights = TreeMap<TypeId, TreeMap<InlayId, (HighlightStyle, InlayHighlight)>>;
#[derive(Debug)]
pub struct MultiBufferRowMapping {
pub first_group: Option<Range<MultiBufferPoint>>,
pub boundaries: Vec<(MultiBufferPoint, Range<MultiBufferPoint>)>,
pub prev_boundary: Option<(MultiBufferPoint, Range<MultiBufferPoint>)>,
pub source_excerpt_end: MultiBufferPoint,
pub target_excerpt_end: MultiBufferPoint,
pub struct CompanionExcerptPatch {
pub patch: Patch<MultiBufferPoint>,
pub edited_range: Range<MultiBufferPoint>,
pub source_excerpt_range: Range<MultiBufferPoint>,
pub target_excerpt_range: Range<MultiBufferPoint>,
}
pub type ConvertMultiBufferRows = fn(
@@ -163,7 +162,7 @@ pub type ConvertMultiBufferRows = fn(
&MultiBufferSnapshot,
&MultiBufferSnapshot,
(Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
) -> Vec<MultiBufferRowMapping>;
) -> Vec<CompanionExcerptPatch>;
/// Decides how text in a [`MultiBuffer`] should be displayed in a buffer, handling inlay hints,
/// folding, hard tabs, soft wrapping, custom blocks (like diagnostics), and highlighting.
@@ -233,7 +232,7 @@ impl Companion {
companion_snapshot: &MultiBufferSnapshot,
our_snapshot: &MultiBufferSnapshot,
bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
) -> Vec<MultiBufferRowMapping> {
) -> Vec<CompanionExcerptPatch> {
let (excerpt_map, convert_fn) = if display_map_id == self.rhs_display_map_id {
(&self.rhs_excerpt_to_lhs_excerpt, self.rhs_rows_to_lhs_rows)
} else {
@@ -242,19 +241,32 @@ impl Companion {
convert_fn(excerpt_map, companion_snapshot, our_snapshot, bounds)
}
pub(crate) fn convert_rows_from_companion(
pub(crate) fn convert_point_from_companion(
&self,
display_map_id: EntityId,
our_snapshot: &MultiBufferSnapshot,
companion_snapshot: &MultiBufferSnapshot,
bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
) -> Vec<MultiBufferRowMapping> {
point: MultiBufferPoint,
) -> Range<MultiBufferPoint> {
let (excerpt_map, convert_fn) = if display_map_id == self.rhs_display_map_id {
(&self.lhs_excerpt_to_rhs_excerpt, self.lhs_rows_to_rhs_rows)
} else {
(&self.rhs_excerpt_to_lhs_excerpt, self.rhs_rows_to_lhs_rows)
};
convert_fn(excerpt_map, our_snapshot, companion_snapshot, bounds)
let excerpt = convert_fn(
excerpt_map,
our_snapshot,
companion_snapshot,
(Bound::Included(point), Bound::Included(point)),
)
.into_iter()
.next();
let Some(excerpt) = excerpt else {
return Point::zero()..our_snapshot.max_point();
};
excerpt.patch.edit_for_old_position(point).new
}
pub(crate) fn companion_excerpt_to_excerpt(
+66 -50
View File
@@ -656,36 +656,25 @@ impl BlockMap {
.to_point(WrapPoint::new(edit.new.end, 0), Bias::Left);
let my_start = companion
.convert_rows_from_companion(
.convert_point_from_companion(
display_map_id,
wrap_snapshot.buffer_snapshot(),
companion_new_snapshot.buffer_snapshot(),
(
Bound::Included(companion_start),
Bound::Included(companion_start),
),
companion_start,
)
.first()
.and_then(|t| t.boundaries.first())
.map(|(_, range)| range.start)
.unwrap_or(wrap_snapshot.buffer_snapshot().max_point());
.start;
let my_end = companion
.convert_rows_from_companion(
.convert_point_from_companion(
display_map_id,
wrap_snapshot.buffer_snapshot(),
companion_new_snapshot.buffer_snapshot(),
(
Bound::Included(companion_end),
Bound::Included(companion_end),
),
companion_end,
)
.first()
.and_then(|t| t.boundaries.last())
.map(|(_, range)| range.end)
.unwrap_or(wrap_snapshot.buffer_snapshot().max_point());
.end;
let mut my_start = wrap_snapshot.make_wrap_point(my_start, Bias::Left);
let mut my_end = wrap_snapshot.make_wrap_point(my_end, Bias::Left);
// TODO(split-diff) should use trailing_excerpt_update_count for the second case
if my_end.column() > 0 || my_end == wrap_snapshot.max_point() {
*my_end.row_mut() += 1;
*my_end.column_mut() = 0;
@@ -1104,7 +1093,7 @@ impl BlockMap {
let our_buffer = wrap_snapshot.buffer_snapshot();
let companion_buffer = companion_snapshot.buffer_snapshot();
let row_mappings = companion.convert_rows_to_companion(
let patches = companion.convert_rows_to_companion(
display_map_id,
companion_buffer,
our_buffer,
@@ -1129,14 +1118,27 @@ impl BlockMap {
let mut result = Vec::new();
for row_mapping in row_mappings {
let mut iter = row_mapping.boundaries.iter().cloned().peekable();
// old approach: buffer_diff computed the patch, and then passed a chosen sequence of points through it, returning the results
// new approach: buffer_diff gives us the patch directly, and then we pass through the points we are interested in
for excerpt in patches {
let mut source_points = (excerpt.edited_range.start.row..=excerpt.edited_range.end.row)
.map(|row| MultiBufferPoint::new(row, 0))
.chain(if excerpt.edited_range.end.column > 0 {
Some(excerpt.edited_range.end)
} else {
None
})
.peekable();
let last_source_point = if excerpt.edited_range.end.column > 0 {
excerpt.edited_range.end
} else {
MultiBufferPoint::new(excerpt.edited_range.end.row, 0)
};
let Some(((first_boundary, first_range), first_group)) =
iter.peek().cloned().zip(row_mapping.first_group.clone())
else {
let Some(first_point) = source_points.peek().copied() else {
continue;
};
let edit_for_first_point = excerpt.patch.edit_for_old_position(first_point);
// Because we calculate spacers based on differences in wrap row
// counts between the RHS and LHS for corresponding buffer points,
@@ -1145,12 +1147,20 @@ impl BlockMap {
// counts should have been balanced already by spacers above this
// edit, so we only need to insert spacers for when the difference
// in counts diverges from that baseline value.
let (our_baseline, their_baseline) = if first_group.start < first_boundary {
(first_group.start, first_range.start)
} else if let Some((prev_boundary, prev_range)) = row_mapping.prev_boundary {
(prev_boundary, prev_range.end)
let (our_baseline, their_baseline) = if edit_for_first_point.old.start < first_point {
// Case 1: We are inside a hunk/group--take the start of the hunk/group on both sides as the baseline.
(
edit_for_first_point.old.start,
edit_for_first_point.new.start,
)
} else if first_point.row > excerpt.source_excerpt_range.start.row {
// Case 2: We are not inside a hunk/group--go back by one row to find the baseline.
let prev_point = Point::new(first_point.row - 1, 0);
let edit_for_prev_point = excerpt.patch.edit_for_old_position(prev_point);
(prev_point, edit_for_prev_point.new.end)
} else {
(first_boundary, first_range.start)
// Case 3: We are at the start of the excerpt--no previous row to use as the baseline.
(first_point, edit_for_first_point.new.start)
};
let our_baseline = wrap_snapshot
.make_wrap_point(our_baseline, Bias::Left)
@@ -1161,14 +1171,17 @@ impl BlockMap {
let mut delta = their_baseline.0 as i32 - our_baseline.0 as i32;
if first_group.start < first_boundary {
let mut current_boundary = first_boundary;
let current_range = first_range;
while let Some((next_boundary, next_range)) = iter.peek().cloned()
&& next_range.end <= current_range.end
{
iter.next();
current_boundary = next_boundary;
// If we started out in the middle of a hunk/group, work up to the end of that group to set up the main loop below.
if edit_for_first_point.old.start < first_point {
let mut current_boundary = first_point;
let current_range = edit_for_first_point.new;
while let Some(next_point) = source_points.peek().cloned() {
let edit_for_next_point = excerpt.patch.edit_for_old_position(next_point);
if edit_for_next_point.new.end > current_range.end {
break;
}
source_points.next();
current_boundary = next_point;
}
let (new_delta, spacer) =
@@ -1187,13 +1200,14 @@ impl BlockMap {
}
}
while let Some((boundary, range)) = iter.next() {
let mut current_boundary = boundary;
let current_range = range;
// Main loop: process one hunk/group at a time, possibly inserting spacers before and after.
while let Some(source_point) = source_points.next() {
let mut current_boundary = source_point;
let current_range = excerpt.patch.edit_for_old_position(current_boundary).new;
// This can only occur at the end of an excerpt.
if current_boundary.column > 0 {
debug_assert_eq!(current_boundary, row_mapping.source_excerpt_end);
debug_assert_eq!(current_boundary, excerpt.source_excerpt_range.end);
break;
}
@@ -1202,9 +1216,12 @@ impl BlockMap {
determine_spacer(current_boundary, current_range.start, delta);
delta = delta_at_start;
while let Some((next_boundary, next_range)) = iter.peek()
&& next_range.end <= current_range.end
{
while let Some(next_point) = source_points.peek().copied() {
let edit_for_next_point = excerpt.patch.edit_for_old_position(next_point);
if edit_for_next_point.new.end > current_range.end {
break;
}
if let Some((wrap_row, height)) = spacer_at_start.take() {
result.push((
BlockPlacement::Above(wrap_row),
@@ -1216,13 +1233,13 @@ impl BlockMap {
));
}
current_boundary = *next_boundary;
iter.next();
current_boundary = next_point;
source_points.next();
}
// This can only occur at the end of an excerpt.
if current_boundary.column > 0 {
debug_assert_eq!(current_boundary, row_mapping.source_excerpt_end);
debug_assert_eq!(current_boundary, excerpt.source_excerpt_range.end);
break;
}
@@ -1254,10 +1271,9 @@ impl BlockMap {
}
}
let (last_boundary, _last_range) = row_mapping.boundaries.last().cloned().unwrap();
if last_boundary == row_mapping.source_excerpt_end {
if last_source_point == excerpt.source_excerpt_range.end {
let (_new_delta, spacer) =
determine_spacer(last_boundary, row_mapping.target_excerpt_end, delta);
determine_spacer(last_source_point, excerpt.target_excerpt_range.end, delta);
if let Some((wrap_row, height)) = spacer {
result.push((
BlockPlacement::Below(wrap_row),
+10 -15
View File
@@ -22742,29 +22742,24 @@ impl Editor {
buffer_ranges.last()
}?;
let start_row_in_buffer = text::ToPoint::to_point(&range.start, buffer).row;
let end_row_in_buffer = text::ToPoint::to_point(&range.end, buffer).row;
let buffer_range = range.to_point(buffer);
let Some(buffer_diff) = multi_buffer.diff_for(buffer.remote_id()) else {
let selection = start_row_in_buffer..end_row_in_buffer;
return Some((multi_buffer.buffer(buffer.remote_id()).unwrap(), selection));
return Some((
multi_buffer.buffer(buffer.remote_id()).unwrap(),
buffer_range.start.row..buffer_range.end.row,
));
};
let buffer_diff_snapshot = buffer_diff.read(cx).snapshot(cx);
let (mut translated, _, _) = buffer_diff_snapshot.points_to_base_text_points(
[
Point::new(start_row_in_buffer, 0),
Point::new(end_row_in_buffer, 0),
],
buffer,
);
let start_row = translated.next().unwrap().start.row;
let end_row = translated.next().unwrap().end.row;
let start =
buffer_diff_snapshot.buffer_point_to_base_text_point(buffer_range.start, buffer);
let end =
buffer_diff_snapshot.buffer_point_to_base_text_point(buffer_range.end, buffer);
Some((
multi_buffer.buffer(buffer.remote_id()).unwrap(),
start_row..end_row,
start.row..end.row,
))
});
+140 -160
View File
@@ -1,4 +1,4 @@
use std::ops::{Bound, Range};
use std::ops::{Bound, Range, RangeInclusive};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use collections::HashMap;
@@ -6,19 +6,19 @@ use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
use gpui::{Action, AppContext as _, Entity, EventEmitter, Focusable, Subscription, WeakEntity};
use language::{Buffer, Capability};
use multi_buffer::{
Anchor, BufferOffset, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer,
MultiBufferPoint, MultiBufferSnapshot, PathKey,
Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, MultiBufferPoint,
MultiBufferSnapshot, PathKey,
};
use project::Project;
use rope::Point;
use text::{OffsetRangeExt as _, ToPoint as _};
use text::{OffsetRangeExt as _, Patch, ToPoint as _};
use ui::{
App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
Styled as _, Window, div,
};
use crate::{
display_map::MultiBufferRowMapping,
display_map::CompanionExcerptPatch,
split_editor_view::{SplitEditorState, SplitEditorView},
};
use workspace::{ActivatePaneLeft, ActivatePaneRight, Item, Workspace};
@@ -35,17 +35,13 @@ pub(crate) fn convert_lhs_rows_to_rhs(
rhs_snapshot: &MultiBufferSnapshot,
lhs_snapshot: &MultiBufferSnapshot,
lhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
) -> Vec<MultiBufferRowMapping> {
convert_rows(
) -> Vec<CompanionExcerptPatch> {
patches_for_range(
lhs_excerpt_to_rhs_excerpt,
lhs_snapshot,
rhs_snapshot,
lhs_bounds,
|diff, points, buffer| {
let (points, first_group, prev_boundary) =
diff.base_text_points_to_points(points, buffer);
(points.collect(), first_group, prev_boundary)
},
|diff, range, buffer| diff.patch_for_base_text_range(range, buffer),
)
}
@@ -54,182 +50,171 @@ pub(crate) fn convert_rhs_rows_to_lhs(
lhs_snapshot: &MultiBufferSnapshot,
rhs_snapshot: &MultiBufferSnapshot,
rhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
) -> Vec<MultiBufferRowMapping> {
convert_rows(
) -> Vec<CompanionExcerptPatch> {
patches_for_range(
rhs_excerpt_to_lhs_excerpt,
rhs_snapshot,
lhs_snapshot,
rhs_bounds,
|diff, points, buffer| {
let (points, first_group, prev_boundary) =
diff.points_to_base_text_points(points, buffer);
(points.collect(), first_group, prev_boundary)
},
|diff, range, buffer| diff.patch_for_buffer_range(range, buffer),
)
}
fn convert_rows<F>(
fn patches_for_range<F>(
excerpt_map: &HashMap<ExcerptId, ExcerptId>,
source_snapshot: &MultiBufferSnapshot,
target_snapshot: &MultiBufferSnapshot,
source_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
translate_fn: F,
) -> Vec<MultiBufferRowMapping>
) -> Vec<CompanionExcerptPatch>
where
F: Fn(
&BufferDiffSnapshot,
Vec<Point>,
&text::BufferSnapshot,
) -> (
Vec<Range<Point>>,
Option<Range<Point>>,
Option<(Point, Range<Point>)>,
),
F: Fn(&BufferDiffSnapshot, RangeInclusive<Point>, &text::BufferSnapshot) -> Patch<Point>,
{
let mut result = Vec::new();
let mut patches = HashMap::default();
for (buffer, buffer_offset_range, source_excerpt_id) in
for (source_buffer, buffer_offset_range, source_excerpt_id) in
source_snapshot.range_to_buffer_ranges(source_bounds)
{
if let Some(translation) = convert_excerpt_rows(
excerpt_map,
let target_excerpt_id = excerpt_map.get(&source_excerpt_id).copied().unwrap();
let target_buffer = target_snapshot
.buffer_for_excerpt(target_excerpt_id)
.unwrap();
let patch = patches.entry(source_buffer.remote_id()).or_insert_with(|| {
let diff = source_snapshot
.diff_for_buffer_id(source_buffer.remote_id())
.unwrap();
let rhs_buffer = if source_buffer.remote_id() == diff.base_text().remote_id() {
&target_buffer
} else {
source_buffer
};
// TODO(split-diff) pass only the union of the ranges for the affected excerpts
translate_fn(diff, Point::zero()..=source_buffer.max_point(), rhs_buffer)
});
let buffer_point_range = buffer_offset_range.to_point(source_buffer);
// TODO(split-diff) maybe narrow the patch to only the edited part of the excerpt
// (less useful for project diff, but important if we want to do singleton side-by-side diff)
result.push(patch_for_excerpt(
source_snapshot,
target_snapshot,
source_excerpt_id,
buffer,
buffer_offset_range,
&translate_fn,
) {
result.push(translation);
}
target_excerpt_id,
source_buffer,
target_buffer,
patch,
buffer_point_range,
));
}
result
}
fn convert_excerpt_rows<F>(
excerpt_map: &HashMap<ExcerptId, ExcerptId>,
fn patch_for_excerpt(
source_snapshot: &MultiBufferSnapshot,
target_snapshot: &MultiBufferSnapshot,
source_excerpt_id: ExcerptId,
target_excerpt_id: ExcerptId,
source_buffer: &text::BufferSnapshot,
source_buffer_range: Range<BufferOffset>,
translate_fn: F,
) -> Option<MultiBufferRowMapping>
where
F: Fn(
&BufferDiffSnapshot,
Vec<Point>,
&text::BufferSnapshot,
) -> (
Vec<Range<Point>>,
Option<Range<Point>>,
Option<(Point, Range<Point>)>,
),
{
let target_excerpt_id = excerpt_map.get(&source_excerpt_id).copied()?;
let target_buffer = target_snapshot.buffer_for_excerpt(target_excerpt_id)?;
let diff = source_snapshot.diff_for_buffer_id(source_buffer.remote_id())?;
let rhs_buffer = if source_buffer.remote_id() == diff.base_text().remote_id() {
&target_buffer
} else {
source_buffer
};
let local_start = source_buffer.offset_to_point(source_buffer_range.start.0);
let local_end = source_buffer.offset_to_point(source_buffer_range.end.0);
let mut input_points: Vec<Point> = (local_start.row..=local_end.row)
.map(|row| Point::new(row, 0))
.collect();
if local_end.column > 0 {
input_points.push(local_end);
}
let (translated_ranges, first_group, prev_boundary) =
translate_fn(&diff, input_points.clone(), rhs_buffer);
let source_multibuffer_range = source_snapshot.range_for_excerpt(source_excerpt_id)?;
target_buffer: &text::BufferSnapshot,
patch: &Patch<Point>,
source_edited_range: Range<Point>,
) -> CompanionExcerptPatch {
let source_multibuffer_range = source_snapshot
.range_for_excerpt(source_excerpt_id)
.unwrap();
let source_excerpt_start_in_multibuffer = source_multibuffer_range.start;
let source_context_range = source_snapshot.context_range_for_excerpt(source_excerpt_id)?;
let source_context_range = source_snapshot
.context_range_for_excerpt(source_excerpt_id)
.unwrap();
let source_excerpt_start_in_buffer = source_context_range.start.to_point(&source_buffer);
let source_excerpt_end_in_buffer = source_context_range.end.to_point(&source_buffer);
let target_multibuffer_range = target_snapshot.range_for_excerpt(target_excerpt_id)?;
let target_multibuffer_range = target_snapshot
.range_for_excerpt(target_excerpt_id)
.unwrap();
let target_excerpt_start_in_multibuffer = target_multibuffer_range.start;
let target_context_range = target_snapshot.context_range_for_excerpt(target_excerpt_id)?;
let target_context_range = target_snapshot
.context_range_for_excerpt(target_excerpt_id)
.unwrap();
let target_excerpt_start_in_buffer = target_context_range.start.to_point(&target_buffer);
let target_excerpt_end_in_buffer = target_context_range.end.to_point(&target_buffer);
let boundaries: Vec<_> = input_points
.into_iter()
.zip(translated_ranges)
.map(|(source_buffer_point, target_range)| {
let source_multibuffer_point = source_excerpt_start_in_multibuffer
+ (source_buffer_point - source_excerpt_start_in_buffer.min(source_buffer_point));
let clamped_target_start = target_range
let edits = patch
.edits()
.iter()
.skip_while(|edit| edit.old.end < source_excerpt_start_in_buffer)
.take_while(|edit| edit.old.start <= source_excerpt_end_in_buffer)
.map(|edit| {
let clamped_source_start = edit
.old
.start
.max(source_excerpt_start_in_buffer)
.min(source_excerpt_end_in_buffer);
let clamped_source_end = edit
.old
.end
.max(source_excerpt_start_in_buffer)
.min(source_excerpt_end_in_buffer);
let source_multibuffer_start = source_excerpt_start_in_multibuffer
+ (clamped_source_start - source_excerpt_start_in_buffer);
let source_multibuffer_end = source_excerpt_start_in_multibuffer
+ (clamped_source_end - source_excerpt_start_in_buffer);
let clamped_target_start = edit
.new
.start
.max(target_excerpt_start_in_buffer)
.min(target_excerpt_end_in_buffer);
let clamped_target_end = target_range
let clamped_target_end = edit
.new
.end
.max(target_excerpt_start_in_buffer)
.min(target_excerpt_end_in_buffer);
let target_multibuffer_start = target_excerpt_start_in_multibuffer
+ (clamped_target_start - target_excerpt_start_in_buffer);
let target_multibuffer_end = target_excerpt_start_in_multibuffer
+ (clamped_target_end - target_excerpt_start_in_buffer);
text::Edit {
old: source_multibuffer_start..source_multibuffer_end,
new: target_multibuffer_start..target_multibuffer_end,
}
});
(
source_multibuffer_point,
target_multibuffer_start..target_multibuffer_end,
)
})
.collect();
let first_group = first_group.map(|first_group| {
let start = source_excerpt_start_in_multibuffer
+ (first_group.start - source_excerpt_start_in_buffer.min(first_group.start));
let end = source_excerpt_start_in_multibuffer
+ (first_group.end - source_excerpt_start_in_buffer.min(first_group.end));
start..end
});
let edits = [text::Edit {
old: source_excerpt_start_in_multibuffer..source_excerpt_start_in_multibuffer,
new: target_excerpt_start_in_multibuffer..target_excerpt_start_in_multibuffer,
}]
.into_iter()
.chain(edits);
let prev_boundary = prev_boundary.map(|(source_buffer_point, target_range)| {
let source_multibuffer_point = source_excerpt_start_in_multibuffer
+ (source_buffer_point - source_excerpt_start_in_buffer.min(source_buffer_point));
let mut merged_edits: Vec<text::Edit<Point>> = Vec::new();
for edit in edits {
if let Some(last) = merged_edits.last_mut() {
if edit.new.start <= last.new.end {
last.old.end = last.old.end.max(edit.old.end);
last.new.end = last.new.end.max(edit.new.end);
continue;
}
}
merged_edits.push(edit);
}
let clamped_target_start = target_range
.start
.max(target_excerpt_start_in_buffer)
.min(target_excerpt_end_in_buffer);
let clamped_target_end = target_range
.end
.max(target_excerpt_start_in_buffer)
.min(target_excerpt_end_in_buffer);
let edited_range = source_excerpt_start_in_multibuffer
+ (source_edited_range.start - source_excerpt_start_in_buffer)
..source_excerpt_start_in_multibuffer
+ (source_edited_range.end - source_excerpt_start_in_buffer);
let target_multibuffer_start = target_excerpt_start_in_multibuffer
+ (clamped_target_start - target_excerpt_start_in_buffer);
let target_multibuffer_end = target_excerpt_start_in_multibuffer
+ (clamped_target_end - target_excerpt_start_in_buffer);
let source_excerpt_end = source_excerpt_start_in_multibuffer
+ (source_excerpt_end_in_buffer - source_excerpt_start_in_buffer);
let target_excerpt_end = target_excerpt_start_in_multibuffer
+ (target_excerpt_end_in_buffer - target_excerpt_start_in_buffer);
(
source_multibuffer_point,
target_multibuffer_start..target_multibuffer_end,
)
});
Some(MultiBufferRowMapping {
boundaries,
first_group,
prev_boundary,
source_excerpt_end: source_excerpt_start_in_multibuffer
+ (source_excerpt_end_in_buffer - source_excerpt_start_in_buffer),
target_excerpt_end: target_excerpt_start_in_multibuffer
+ (target_excerpt_end_in_buffer - target_excerpt_start_in_buffer),
})
CompanionExcerptPatch {
patch: Patch::new(merged_edits),
edited_range,
source_excerpt_range: source_excerpt_start_in_multibuffer..source_excerpt_end,
target_excerpt_range: target_excerpt_start_in_multibuffer..target_excerpt_end,
}
}
pub struct SplitDiffFeatureFlag;
@@ -623,24 +608,16 @@ impl SplittableEditor {
let source_snapshot = source_multibuffer.read(cx).snapshot(cx);
let target_snapshot = target_multibuffer.read(cx).snapshot(cx);
let target_point = target_editor.update(cx, |target_editor, cx| {
let target_range = target_editor.update(cx, |target_editor, cx| {
target_editor.display_map.update(cx, |display_map, cx| {
let display_map_id = cx.entity_id();
display_map.companion().unwrap().update(cx, |companion, _| {
companion
.convert_rows_from_companion(
display_map_id,
&target_snapshot,
&source_snapshot,
(Bound::Included(source_point), Bound::Included(source_point)),
)
.first()
.unwrap()
.boundaries
.first()
.unwrap()
.1
.start
companion.convert_point_from_companion(
display_map_id,
&target_snapshot,
&source_snapshot,
source_point,
)
})
})
});
@@ -648,7 +625,7 @@ impl SplittableEditor {
target_editor.update(cx, |editor, cx| {
editor.set_suppress_selection_callback(true);
editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([target_point..target_point]);
s.select_ranges([target_range]);
});
editor.set_suppress_selection_callback(false);
});
@@ -1501,14 +1478,17 @@ impl LhsEditor {
.into_iter()
.map(|(_, excerpt_range)| {
let point_range_to_base_text_point_range = |range: Range<Point>| {
let (mut translated, _, _) = diff_snapshot.points_to_base_text_points(
[Point::new(range.start.row, 0), Point::new(range.end.row, 0)],
main_buffer,
);
let start_row = translated.next().unwrap().start.row;
let end_row = translated.next().unwrap().end.row;
let end_column = diff_snapshot.base_text().line_len(end_row);
Point::new(start_row, 0)..Point::new(end_row, end_column)
let start = diff_snapshot
.buffer_point_to_base_text_range(
Point::new(range.start.row, 0),
main_buffer,
)
.start;
let end = diff_snapshot
.buffer_point_to_base_text_range(Point::new(range.end.row, 0), main_buffer)
.end;
let end_column = diff_snapshot.base_text().line_len(end.row);
Point::new(start.row, 0)..Point::new(end.row, end_column)
};
let rhs = excerpt_range.primary.to_point(main_buffer);
let context = excerpt_range.context.to_point(main_buffer);
+40
View File
@@ -225,6 +225,46 @@ where
old
}
}
/// Returns the edit that touches the given old position.
///
/// An edit is considered to touch the given old position if edit.old.start <= old <= edit.old.end (note, inclusive on the right).
///
/// If there are no edits touching the given old position, an empty edit with appropriate (empty) old and new ranges is returned.
pub fn edit_for_old_position(&self, old: T) -> Edit<T> {
let edits = self.edits();
let ix = match edits.binary_search_by(|probe| probe.old.start.cmp(&old)) {
Ok(ix) => ix,
Err(ix) => {
if ix == 0 {
return Edit {
old: old..old,
new: old..old,
};
} else {
ix - 1
}
}
};
if let Some(edit) = edits.get(ix) {
if old > edit.old.end {
let translated = edit.new.end + (old - edit.old.end);
Edit {
new: translated..translated,
old: old..old,
}
} else {
edit.clone()
}
} else {
Edit {
old: old..old,
new: old..old,
}
}
}
}
impl<T> Patch<T> {