@@ -1197,7 +1197,14 @@ fn resolve_selections_point<'a>(
selections.map(move |s| {
let start = summaries.next().unwrap();
let end = summaries.next().unwrap();
- assert!(start <= end, "start: {:?}, end: {:?}", start, end);
+ assert!(
+ start <= end,
+ "anchors: start: {:?}, end: {:?}; resolved to: start: {:?}, end: {:?}",
+ s.start,
+ s.end,
+ start,
+ end
+ );
Selection {
id: s.id,
start,
@@ -5347,8 +5347,8 @@ impl MultiBufferSnapshot {
}
}
+ let excerpt_start_position = ExcerptDimension(start);
if self.diff_transforms.is_empty() {
- let excerpt_start_position = ExcerptDimension(start);
if let Some(excerpt) = item {
if excerpt.id != excerpt_id && excerpt_id != ExcerptId::max() {
return excerpt_start_position.0;
@@ -5382,10 +5382,9 @@ impl MultiBufferSnapshot {
.cursor::<Dimensions<ExcerptDimension<MBD>, OutputDimension<MBD>>>(());
diff_transforms_cursor.next();
- let excerpt_start_position = ExcerptDimension(start);
if let Some(excerpt) = item {
if excerpt.id != excerpt_id && excerpt_id != ExcerptId::max() {
- return self.resolve_summary_for_anchor(
+ return self.resolve_summary_for_min_or_max_anchor(
&Anchor::min(),
excerpt_start_position,
&mut diff_transforms_cursor,
@@ -5413,10 +5412,15 @@ impl MultiBufferSnapshot {
if diff_transforms_cursor.start().0 < position {
diff_transforms_cursor.seek_forward(&position, Bias::Left);
}
- self.resolve_summary_for_anchor(&anchor, position, &mut diff_transforms_cursor)
+ self.resolve_summary_for_anchor(
+ &anchor,
+ position,
+ &mut diff_transforms_cursor,
+ &excerpt.buffer,
+ )
} else {
diff_transforms_cursor.seek_forward(&excerpt_start_position, Bias::Left);
- self.resolve_summary_for_anchor(
+ self.resolve_summary_for_min_or_max_anchor(
&Anchor::max(),
excerpt_start_position,
&mut diff_transforms_cursor,
@@ -5425,6 +5429,9 @@ impl MultiBufferSnapshot {
}
}
+ /// Maps an anchor's excerpt-space position to its output-space position by
+ /// walking the diff transforms. The cursor is shared across consecutive
+ /// calls, so it may already be partway through the transform list.
fn resolve_summary_for_anchor<MBD>(
&self,
anchor: &Anchor,
@@ -5433,24 +5440,29 @@ impl MultiBufferSnapshot {
DiffTransform,
Dimensions<ExcerptDimension<MBD>, OutputDimension<MBD>>,
>,
+ excerpt_buffer: &text::BufferSnapshot,
) -> MBD
where
MBD: MultiBufferDimension + Ord + Sub + AddAssign<<MBD as Sub>::Output>,
{
loop {
let transform_end_position = diff_transforms.end().0;
- let at_transform_end =
- transform_end_position == excerpt_position && diff_transforms.item().is_some();
- if at_transform_end && anchor.text_anchor.bias == Bias::Right {
+ let item = diff_transforms.item();
+ let at_transform_end = transform_end_position == excerpt_position && item.is_some();
+
+ // A right-biased anchor at a transform boundary belongs to the
+ // *next* transform, so advance past the current one.
+ if anchor.text_anchor.bias == Bias::Right && at_transform_end {
diff_transforms.next();
continue;
}
let mut position = diff_transforms.start().1;
- match diff_transforms.item() {
+ match item {
Some(DiffTransform::DeletedHunk {
buffer_id,
base_text_byte_range,
+ hunk_info,
..
}) => {
if let Some(diff_base_anchor) = &anchor.diff_base_anchor
@@ -5458,6 +5470,8 @@ impl MultiBufferSnapshot {
self.diffs.get(buffer_id).map(|diff| diff.base_text())
&& diff_base_anchor.is_valid(&base_text)
{
+ // The anchor carries a diff-base position — resolve it
+ // to a location inside the deleted hunk.
let base_text_offset = diff_base_anchor.to_offset(base_text);
if base_text_offset >= base_text_byte_range.start
&& base_text_offset <= base_text_byte_range.end
@@ -5468,12 +5482,34 @@ impl MultiBufferSnapshot {
);
position.0.add_text_dim(&position_in_hunk);
} else if at_transform_end {
+ // diff_base offset falls outside this hunk's range;
+ // advance to see if the next transform is a better fit.
diff_transforms.next();
continue;
}
+ } else if at_transform_end
+ && anchor
+ .text_anchor
+ .cmp(&hunk_info.hunk_start_anchor, excerpt_buffer)
+ .is_gt()
+ {
+ // The anchor has no (valid) diff-base position, so it
+ // belongs in the buffer content, not in the deleted
+ // hunk. However, after an edit deletes the text between
+ // the hunk boundary and this anchor, both resolve to
+ // the same excerpt_position—landing us here on the
+ // DeletedHunk left behind by the shared cursor. Use the
+ // CRDT ordering to detect that the anchor is strictly
+ // *past* the hunk boundary and skip to the following
+ // BufferContent.
+ diff_transforms.next();
+ continue;
}
}
_ => {
+ // On a BufferContent (or no transform). If the anchor
+ // carries a diff_base_anchor it needs a DeletedHunk, so
+ // advance to find one.
if at_transform_end && anchor.diff_base_anchor.is_some() {
diff_transforms.next();
continue;
@@ -5487,6 +5523,41 @@ impl MultiBufferSnapshot {
}
}
+ /// Like `resolve_summary_for_anchor` but optimized for min/max anchors.
+ fn resolve_summary_for_min_or_max_anchor<MBD>(
+ &self,
+ anchor: &Anchor,
+ excerpt_position: ExcerptDimension<MBD>,
+ diff_transforms: &mut Cursor<
+ DiffTransform,
+ Dimensions<ExcerptDimension<MBD>, OutputDimension<MBD>>,
+ >,
+ ) -> MBD
+ where
+ MBD: MultiBufferDimension + Ord + Sub + AddAssign<<MBD as Sub>::Output>,
+ {
+ loop {
+ let transform_end_position = diff_transforms.end().0;
+ let item = diff_transforms.item();
+ let at_transform_end = transform_end_position == excerpt_position && item.is_some();
+
+ // A right-biased anchor at a transform boundary belongs to the
+ // *next* transform, so advance past the current one.
+ if anchor.text_anchor.bias == Bias::Right && at_transform_end {
+ diff_transforms.next();
+ continue;
+ }
+
+ let mut position = diff_transforms.start().1;
+ if let Some(DiffTransform::BufferContent { .. }) | None = item {
+ let overshoot = excerpt_position - diff_transforms.start().0;
+ position += overshoot;
+ }
+
+ return position.0;
+ }
+ }
+
fn excerpt_offset_for_anchor(&self, anchor: &Anchor) -> ExcerptOffset {
let mut cursor = self
.excerpts
@@ -5557,7 +5628,7 @@ impl MultiBufferSnapshot {
let excerpt_start_position = ExcerptDimension(MBD::from_summary(&cursor.start().text));
if let Some(excerpt) = cursor.item() {
if excerpt.id != excerpt_id && excerpt_id != ExcerptId::max() {
- let position = self.resolve_summary_for_anchor(
+ let position = self.resolve_summary_for_min_or_max_anchor(
&Anchor::min(),
excerpt_start_position,
&mut diff_transforms_cursor,
@@ -5595,11 +5666,12 @@ impl MultiBufferSnapshot {
anchor,
position,
&mut diff_transforms_cursor,
+ &excerpt.buffer,
));
}
} else {
diff_transforms_cursor.seek_forward(&excerpt_start_position, Bias::Left);
- let position = self.resolve_summary_for_anchor(
+ let position = self.resolve_summary_for_min_or_max_anchor(
&Anchor::max(),
excerpt_start_position,
&mut diff_transforms_cursor,
@@ -205,7 +205,8 @@ fn expand_changed_word_selection(
mod test {
use indoc::indoc;
- use crate::test::NeovimBackedTestContext;
+ use crate::state::Mode;
+ use crate::test::{NeovimBackedTestContext, VimTestContext};
#[gpui::test]
async fn test_change_h(cx: &mut gpui::TestAppContext) {
@@ -703,4 +704,34 @@ mod test {
.assert_matches();
}
}
+
+ #[gpui::test]
+ async fn test_change_with_selection_spanning_expanded_diff_hunk(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+
+ let diff_base = indoc! {"
+ fn main() {
+ println!(\"old\");
+ }
+ "};
+
+ cx.set_state(
+ indoc! {"
+ fn main() {
+ ˇprintln!(\"new\");
+ }
+ "},
+ Mode::Normal,
+ );
+ cx.set_head_text(diff_base);
+ cx.update_editor(|editor, window, cx| {
+ editor.expand_all_diff_hunks(&editor::actions::ExpandAllDiffHunks, window, cx);
+ });
+
+ // Enter visual mode and move up so the selection spans from the
+ // insertion (current line) into the deletion (diff base line).
+ // Then press `c` which in visual mode dispatches `vim::Substitute`,
+ // performing the change operation across the insertion/deletion boundary.
+ cx.simulate_keystrokes("v k c");
+ }
}