From 45146b6f30f6c8c1fa4bd076fae34ad2b4e886d4 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 24 Feb 2025 23:13:13 -0500 Subject: [PATCH] Implement staging of partially-staged hunks (#25520) Closes: #25475 This PR makes it possible to stage uncommitted hunks that overlap but do not coincide with an unstaged hunk. Release Notes: - Made it possible to stage hunks that are already partially staged --------- Co-authored-by: Max Brunsfeld Co-authored-by: Max --- crates/buffer_diff/src/buffer_diff.rs | 453 +++++++++++++----- crates/editor/src/editor.rs | 33 +- crates/editor/src/editor_tests.rs | 106 +++- crates/editor/src/test/editor_test_context.rs | 15 +- crates/multi_buffer/src/multi_buffer.rs | 3 - crates/text/src/text.rs | 1 + 6 files changed, 432 insertions(+), 179 deletions(-) diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 7223bb7086bbf3..cc1767b4cbb553 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -3,7 +3,8 @@ use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter}; use language::{Language, LanguageRegistry}; use rope::Rope; -use std::{cmp, future::Future, iter, ops::Range, sync::Arc}; +use std::cmp::Ordering; +use std::{future::Future, iter, ops::Range, sync::Arc}; use sum_tree::SumTree; use text::ToOffset as _; use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point}; @@ -68,7 +69,6 @@ pub struct DiffHunk { /// The range in the buffer's diff base text to which this hunk corresponds. pub diff_base_byte_range: Range, pub secondary_status: DiffHunkSecondaryStatus, - pub secondary_diff_base_byte_range: Option>, } /// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range. @@ -110,12 +110,17 @@ impl sum_tree::Summary for DiffHunkSummary { } impl<'a> sum_tree::SeekTarget<'a, DiffHunkSummary, DiffHunkSummary> for Anchor { - fn cmp( - &self, - cursor_location: &DiffHunkSummary, - buffer: &text::BufferSnapshot, - ) -> cmp::Ordering { - self.cmp(&cursor_location.buffer_range.end, buffer) + fn cmp(&self, cursor_location: &DiffHunkSummary, buffer: &text::BufferSnapshot) -> Ordering { + if self + .cmp(&cursor_location.buffer_range.start, buffer) + .is_lt() + { + Ordering::Less + } else if self.cmp(&cursor_location.buffer_range.end, buffer).is_gt() { + Ordering::Greater + } else { + Ordering::Equal + } } } @@ -171,97 +176,96 @@ impl BufferDiffSnapshot { } } - fn buffer_range_to_unchanged_diff_base_range( - &self, - buffer_range: Range, - buffer: &text::BufferSnapshot, - ) -> Option> { - let mut hunks = self.inner.hunks.iter(); - let mut start = 0; - let mut pos = buffer.anchor_before(0); - while let Some(hunk) = hunks.next() { - assert!(buffer_range.start.cmp(&pos, buffer).is_ge()); - assert!(hunk.buffer_range.start.cmp(&pos, buffer).is_ge()); - if hunk - .buffer_range - .start - .cmp(&buffer_range.end, buffer) - .is_ge() - { - // target buffer range is contained in the unchanged stretch leading up to this next hunk, - // so do a final adjustment based on that - break; - } - - // if the target buffer range intersects this hunk at all, no dice - if buffer_range - .start - .cmp(&hunk.buffer_range.end, buffer) - .is_lt() - { - return None; - } - - start += hunk.buffer_range.start.to_offset(buffer) - pos.to_offset(buffer); - start += hunk.diff_base_byte_range.end - hunk.diff_base_byte_range.start; - pos = hunk.buffer_range.end; - } - start += buffer_range.start.to_offset(buffer) - pos.to_offset(buffer); - let end = start + buffer_range.end.to_offset(buffer) - buffer_range.start.to_offset(buffer); - Some(start..end) - } - - pub fn secondary_edits_for_stage_or_unstage( + pub fn new_secondary_text_for_stage_or_unstage( &self, stage: bool, - hunks: impl Iterator, Option>, Range)>, + hunks: impl Iterator, Range)>, buffer: &text::BufferSnapshot, - ) -> Vec<(Range, String)> { - let Some(secondary_diff) = self.secondary_diff() else { - log::debug!("no secondary diff"); - return Vec::new(); + cx: &mut App, + ) -> Option { + let secondary_diff = self.secondary_diff()?; + let index_base = if let Some(index_base) = secondary_diff.base_text() { + index_base.text.as_rope().clone() + } else if stage { + Rope::from("") + } else { + return None; }; - let index_base = secondary_diff.base_text().map_or_else( - || Rope::from(""), - |snapshot| snapshot.text.as_rope().clone(), - ); let head_base = self.base_text().map_or_else( || Rope::from(""), |snapshot| snapshot.text.as_rope().clone(), ); - log::debug!("original: {:?}", index_base.to_string()); + + let mut secondary_cursor = secondary_diff.inner.hunks.cursor::(buffer); + secondary_cursor.next(buffer); let mut edits = Vec::new(); - for (diff_base_byte_range, secondary_diff_base_byte_range, buffer_range) in hunks { - let (index_byte_range, replacement_text) = if stage { + let mut prev_secondary_hunk_buffer_offset = 0; + let mut prev_secondary_hunk_base_text_offset = 0; + for (buffer_range, diff_base_byte_range) in hunks { + let skipped_hunks = secondary_cursor.slice(&buffer_range.start, Bias::Left, buffer); + + if let Some(secondary_hunk) = skipped_hunks.last() { + prev_secondary_hunk_base_text_offset = secondary_hunk.diff_base_byte_range.end; + prev_secondary_hunk_buffer_offset = + secondary_hunk.buffer_range.end.to_offset(buffer); + } + + let mut buffer_offset_range = buffer_range.to_offset(buffer); + let start_overshoot = buffer_offset_range.start - prev_secondary_hunk_buffer_offset; + let mut secondary_base_text_start = + prev_secondary_hunk_base_text_offset + start_overshoot; + + while let Some(secondary_hunk) = secondary_cursor.item().filter(|item| { + item.buffer_range + .start + .cmp(&buffer_range.end, buffer) + .is_le() + }) { + let secondary_hunk_offset_range = secondary_hunk.buffer_range.to_offset(buffer); + prev_secondary_hunk_base_text_offset = secondary_hunk.diff_base_byte_range.end; + prev_secondary_hunk_buffer_offset = secondary_hunk_offset_range.end; + + secondary_base_text_start = + secondary_base_text_start.min(secondary_hunk.diff_base_byte_range.start); + buffer_offset_range.start = buffer_offset_range + .start + .min(secondary_hunk_offset_range.start); + + secondary_cursor.next(buffer); + } + + let end_overshoot = buffer_offset_range + .end + .saturating_sub(prev_secondary_hunk_buffer_offset); + let secondary_base_text_end = prev_secondary_hunk_base_text_offset + end_overshoot; + + let secondary_base_text_range = secondary_base_text_start..secondary_base_text_end; + buffer_offset_range.end = buffer_offset_range + .end + .max(prev_secondary_hunk_buffer_offset); + + let replacement_text = if stage { log::debug!("staging"); - let mut replacement_text = String::new(); - let Some(index_byte_range) = secondary_diff_base_byte_range.clone() else { - log::debug!("not a stageable hunk"); - continue; - }; - log::debug!("using {:?}", index_byte_range); - for chunk in buffer.text_for_range(buffer_range.clone()) { - replacement_text.push_str(chunk); - } - (index_byte_range, replacement_text) + buffer + .text_for_range(buffer_offset_range) + .collect::() } else { log::debug!("unstaging"); - let mut replacement_text = String::new(); - let Some(index_byte_range) = secondary_diff - .buffer_range_to_unchanged_diff_base_range(buffer_range.clone(), &buffer) - else { - log::debug!("not an unstageable hunk"); - continue; - }; - for chunk in head_base.chunks_in_range(diff_base_byte_range.clone()) { - replacement_text.push_str(chunk); - } - (index_byte_range, replacement_text) + head_base + .chunks_in_range(diff_base_byte_range.clone()) + .collect::() }; - edits.push((index_byte_range, replacement_text)); + edits.push((secondary_base_text_range, replacement_text)); } - log::debug!("edits: {edits:?}"); - edits + + let buffer = cx.new(|cx| { + language::Buffer::local_normalized(index_base, text::LineEnding::default(), cx) + }); + let new_text = buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + buffer.as_rope().clone() + }); + Some(new_text) } } @@ -322,13 +326,12 @@ impl BufferDiffInner { } let mut secondary_status = DiffHunkSecondaryStatus::None; - let mut secondary_diff_base_byte_range = None; if let Some(secondary_cursor) = secondary_cursor.as_mut() { if start_anchor .cmp(&secondary_cursor.start().buffer_range.start, buffer) .is_gt() { - secondary_cursor.seek_forward(&end_anchor, Bias::Left, buffer); + secondary_cursor.seek_forward(&start_anchor, Bias::Left, buffer); } if let Some(secondary_hunk) = secondary_cursor.item() { @@ -339,12 +342,12 @@ impl BufferDiffInner { } if secondary_range == (start_point..end_point) { secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk; - secondary_diff_base_byte_range = - Some(secondary_hunk.diff_base_byte_range.clone()); } else if secondary_range.start <= end_point { secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk; } } + } else { + log::debug!("no secondary cursor!!"); } return Some(DiffHunk { @@ -352,7 +355,6 @@ impl BufferDiffInner { diff_base_byte_range: start_base..end_base, buffer_range: start_anchor..end_anchor, secondary_status, - secondary_diff_base_byte_range, }); }) } @@ -387,7 +389,6 @@ impl BufferDiffInner { buffer_range: hunk.buffer_range.clone(), // The secondary status is not used by callers of this method. secondary_status: DiffHunkSecondaryStatus::None, - secondary_diff_base_byte_range: None, }) }) } @@ -408,12 +409,12 @@ impl BufferDiffInner { .start .cmp(&old_hunk.buffer_range.start, new_snapshot) { - cmp::Ordering::Less => { + Ordering::Less => { start.get_or_insert(new_hunk.buffer_range.start); end.replace(new_hunk.buffer_range.end); new_cursor.next(new_snapshot); } - cmp::Ordering::Equal => { + Ordering::Equal => { if new_hunk != old_hunk { start.get_or_insert(new_hunk.buffer_range.start); if old_hunk @@ -431,7 +432,7 @@ impl BufferDiffInner { new_cursor.next(new_snapshot); old_cursor.next(new_snapshot); } - cmp::Ordering::Greater => { + Ordering::Greater => { start.get_or_insert(old_hunk.buffer_range.start); end.replace(old_hunk.buffer_range.end); old_cursor.next(new_snapshot); @@ -1059,6 +1060,7 @@ mod tests { use rand::{rngs::StdRng, Rng as _}; use text::{Buffer, BufferId, Rope}; use unindent::Unindent as _; + use util::test::marked_text_ranges; #[ctor::ctor] fn init_logger() { @@ -1257,6 +1259,208 @@ mod tests { ); } + #[gpui::test] + async fn test_stage_hunk(cx: &mut TestAppContext) { + struct Example { + name: &'static str, + head_text: String, + index_text: String, + buffer_marked_text: String, + final_index_text: String, + } + + let table = [ + Example { + name: "uncommitted hunk straddles end of unstaged hunk", + head_text: " + one + two + three + four + five + " + .unindent(), + index_text: " + one + TWO_HUNDRED + three + FOUR_HUNDRED + five + " + .unindent(), + buffer_marked_text: " + ZERO + one + two + «THREE_HUNDRED + FOUR_HUNDRED» + five + SIX + " + .unindent(), + final_index_text: " + one + two + THREE_HUNDRED + FOUR_HUNDRED + five + " + .unindent(), + }, + Example { + name: "uncommitted hunk straddles start of unstaged hunk", + head_text: " + one + two + three + four + five + " + .unindent(), + index_text: " + one + TWO_HUNDRED + three + FOUR_HUNDRED + five + " + .unindent(), + buffer_marked_text: " + ZERO + one + «TWO_HUNDRED + THREE_HUNDRED» + four + five + SIX + " + .unindent(), + final_index_text: " + one + TWO_HUNDRED + THREE_HUNDRED + four + five + " + .unindent(), + }, + Example { + name: "uncommitted hunk strictly contains unstaged hunks", + head_text: " + one + two + three + four + five + six + seven + " + .unindent(), + index_text: " + one + TWO + THREE + FOUR + FIVE + SIX + seven + " + .unindent(), + buffer_marked_text: " + one + TWO + «THREE_HUNDRED + FOUR + FIVE_HUNDRED» + SIX + seven + " + .unindent(), + final_index_text: " + one + TWO + THREE_HUNDRED + FOUR + FIVE_HUNDRED + SIX + seven + " + .unindent(), + }, + Example { + name: "uncommitted deletion hunk", + head_text: " + one + two + three + four + five + " + .unindent(), + index_text: " + one + two + three + four + five + " + .unindent(), + buffer_marked_text: " + one + ˇfive + " + .unindent(), + final_index_text: " + one + five + " + .unindent(), + }, + ]; + + for example in table { + let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false); + let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); + let uncommitted_diff = + BufferDiff::build_sync(buffer.clone(), example.head_text.clone(), cx); + let unstaged_diff = + BufferDiff::build_sync(buffer.clone(), example.index_text.clone(), cx); + let uncommitted_diff = BufferDiffSnapshot { + inner: uncommitted_diff, + secondary_diff: Some(Box::new(BufferDiffSnapshot { + inner: unstaged_diff, + is_single_insertion: false, + secondary_diff: None, + })), + is_single_insertion: false, + }; + + let range = buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end); + + let new_index_text = cx + .update(|cx| { + uncommitted_diff.new_secondary_text_for_stage_or_unstage( + true, + uncommitted_diff + .hunks_intersecting_range(range, &buffer) + .map(|hunk| { + (hunk.buffer_range.clone(), hunk.diff_base_byte_range.clone()) + }), + &buffer, + cx, + ) + }) + .unwrap() + .to_string(); + pretty_assertions::assert_eq!( + new_index_text, + example.final_index_text, + "example: {}", + example.name + ); + } + } + #[gpui::test] async fn test_buffer_diff_compare(cx: &mut TestAppContext) { let base_text = " @@ -1382,7 +1586,7 @@ mod tests { } #[gpui::test(iterations = 100)] - async fn test_secondary_edits_for_stage_unstage(cx: &mut TestAppContext, mut rng: StdRng) { + async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) { fn gen_line(rng: &mut StdRng) -> String { if rng.gen_bool(0.2) { "\n".to_owned() @@ -1447,7 +1651,7 @@ mod tests { fn uncommitted_diff( working_copy: &language::BufferSnapshot, - index_text: &Entity, + index_text: &Rope, head_text: String, cx: &mut TestAppContext, ) -> BufferDiff { @@ -1456,7 +1660,7 @@ mod tests { buffer_id: working_copy.remote_id(), inner: BufferDiff::build_sync( working_copy.text.clone(), - index_text.read_with(cx, |index_text, _| index_text.text()), + index_text.to_string(), cx, ), secondary_diff: None, @@ -1487,17 +1691,11 @@ mod tests { ) }); let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot()); - let index_text = cx.new(|cx| { - language::Buffer::local_normalized( - if rng.gen() { - Rope::from(head_text.as_str()) - } else { - working_copy.as_rope().clone() - }, - text::LineEnding::default(), - cx, - ) - }); + let mut index_text = if rng.gen() { + Rope::from(head_text.as_str()) + } else { + working_copy.as_rope().clone() + }; let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx); let mut hunks = cx.update(|cx| { @@ -1511,37 +1709,29 @@ mod tests { for _ in 0..operations { let i = rng.gen_range(0..hunks.len()); let hunk = &mut hunks[i]; - let hunk_fields = ( - hunk.diff_base_byte_range.clone(), - hunk.secondary_diff_base_byte_range.clone(), - hunk.buffer_range.clone(), - ); - let stage = match ( - hunk.secondary_status, - hunk.secondary_diff_base_byte_range.clone(), - ) { - (DiffHunkSecondaryStatus::HasSecondaryHunk, Some(_)) => { + let stage = match hunk.secondary_status { + DiffHunkSecondaryStatus::HasSecondaryHunk => { hunk.secondary_status = DiffHunkSecondaryStatus::None; - hunk.secondary_diff_base_byte_range = None; true } - (DiffHunkSecondaryStatus::None, None) => { + DiffHunkSecondaryStatus::None => { hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk; - // We don't look at this, just notice whether it's Some or not. - hunk.secondary_diff_base_byte_range = Some(17..17); false } _ => unreachable!(), }; let snapshot = cx.update(|cx| diff.snapshot(cx)); - let edits = snapshot.secondary_edits_for_stage_or_unstage( - stage, - [hunk_fields].into_iter(), - &working_copy, - ); - index_text.update(cx, |index_text, cx| { - index_text.edit(edits, None, cx); + index_text = cx.update(|cx| { + snapshot + .new_secondary_text_for_stage_or_unstage( + stage, + [(hunk.buffer_range.clone(), hunk.diff_base_byte_range.clone())] + .into_iter(), + &working_copy, + cx, + ) + .unwrap() }); diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx); @@ -1550,6 +1740,7 @@ mod tests { .collect::>() }); assert_eq!(hunks.len(), found_hunks.len()); + for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) { assert_eq!( expected_hunk.buffer_range.to_point(&working_copy), @@ -1560,10 +1751,6 @@ mod tests { found_hunk.diff_base_byte_range ); assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status); - assert_eq!( - expected_hunk.secondary_diff_base_byte_range.is_some(), - found_hunk.secondary_diff_base_byte_range.is_some() - ) } hunks = found_hunks; } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3fa4997ab69718..62c3b39fde93d3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -13329,7 +13329,7 @@ impl Editor { snapshot: &MultiBufferSnapshot, ) -> bool { let mut hunks = self.diff_hunks_in_ranges(ranges, &snapshot); - hunks.any(|hunk| hunk.secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk) + hunks.any(|hunk| hunk.secondary_status != DiffHunkSecondaryStatus::None) } pub fn toggle_staged_selected_diff_hunks( @@ -13474,12 +13474,8 @@ impl Editor { log::debug!("no diff for buffer id"); return; }; - let Some(secondary_diff) = diff.secondary_diff() else { - log::debug!("no secondary diff for buffer id"); - return; - }; - let edits = diff.secondary_edits_for_stage_or_unstage( + let Some(new_index_text) = diff.new_secondary_text_for_stage_or_unstage( stage, hunks.filter_map(|hunk| { if stage && hunk.secondary_status == DiffHunkSecondaryStatus::None { @@ -13489,29 +13485,14 @@ impl Editor { { return None; } - Some(( - hunk.diff_base_byte_range.clone(), - hunk.secondary_diff_base_byte_range.clone(), - hunk.buffer_range.clone(), - )) + Some((hunk.buffer_range.clone(), hunk.diff_base_byte_range.clone())) }), &buffer_snapshot, - ); - - let Some(index_base) = secondary_diff - .base_text() - .map(|snapshot| snapshot.text.as_rope().clone()) - else { - log::debug!("no index base"); + cx, + ) else { + log::debug!("missing secondary diff or index text"); return; }; - let index_buffer = cx.new(|cx| { - Buffer::local_normalized(index_base.clone(), text::LineEnding::default(), cx) - }); - let new_index_text = index_buffer.update(cx, |index_buffer, cx| { - index_buffer.edit(edits, None, cx); - index_buffer.snapshot().as_rope().to_string() - }); let new_index_text = if new_index_text.is_empty() && !stage && (diff.is_single_insertion @@ -13531,7 +13512,7 @@ impl Editor { cx.background_spawn( repo.read(cx) - .set_index_text(&path, new_index_text) + .set_index_text(&path, new_index_text.map(|rope| rope.to_string())) .log_err(), ) .detach(); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 13380c45dd036c..aeff3dcc61315d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -7,7 +7,7 @@ use crate::{ }, JoinLines, }; -use buffer_diff::{BufferDiff, DiffHunkStatus}; +use buffer_diff::{BufferDiff, DiffHunkStatus, DiffHunkStatusKind}; use futures::StreamExt; use gpui::{ div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, @@ -3389,7 +3389,7 @@ async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &m .unindent(), ); - cx.set_diff_base(&diff_base); + cx.set_head_text(&diff_base); executor.run_until_parked(); // Join lines @@ -3429,7 +3429,7 @@ async fn test_custom_newlines_cause_no_false_positive_diffs( init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3"); - cx.set_diff_base("Line 0\r\nLine 1\r\nLine 2\r\nLine 3"); + cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3"); executor.run_until_parked(); cx.update_editor(|editor, window, cx| { @@ -5811,7 +5811,7 @@ async fn test_fold_function_bodies(cx: &mut TestAppContext) { let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await; cx.set_state(&text); - cx.set_diff_base(&base_text); + cx.set_head_text(&base_text); cx.update_editor(|editor, window, cx| { editor.expand_all_diff_hunks(&Default::default(), window, cx); }); @@ -11039,7 +11039,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) .unindent(), ); - cx.set_diff_base(&diff_base); + cx.set_head_text(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, window, cx| { @@ -12531,7 +12531,7 @@ async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) { three "#}; - cx.set_diff_base(base_text); + cx.set_head_text(base_text); cx.set_state("\nˇ\n"); cx.executor().run_until_parked(); cx.update_editor(|editor, _window, cx| { @@ -13168,7 +13168,7 @@ async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut .unindent(), ); - cx.set_diff_base(&diff_base); + cx.set_head_text(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, window, cx| { @@ -13302,7 +13302,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks( .unindent(), ); - cx.set_diff_base(&diff_base); + cx.set_head_text(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, window, cx| { @@ -13330,7 +13330,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks( .unindent(), ); - cx.set_diff_base("new diff base!"); + cx.set_head_text("new diff base!"); executor.run_until_parked(); cx.assert_state_with_diff( r#" @@ -13630,7 +13630,7 @@ async fn test_edits_around_expanded_insertion_hunks( .unindent(), ); - cx.set_diff_base(&diff_base); + cx.set_head_text(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, window, cx| { @@ -13778,7 +13778,7 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - cx.set_diff_base(indoc! { " + cx.set_head_text(indoc! { " one two three @@ -13901,7 +13901,7 @@ async fn test_edits_around_expanded_deletion_hunks( .unindent(), ); - cx.set_diff_base(&diff_base); + cx.set_head_text(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, window, cx| { @@ -14024,7 +14024,7 @@ async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &m .unindent(), ); - cx.set_diff_base(&base_text); + cx.set_head_text(&base_text); executor.run_until_parked(); cx.update_editor(|editor, window, cx| { @@ -14106,7 +14106,7 @@ async fn test_edit_after_expanded_modification_hunk( .unindent(), ); - cx.set_diff_base(&diff_base); + cx.set_head_text(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, window, cx| { editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx); @@ -14841,7 +14841,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp "# .unindent(), ); - cx.set_diff_base(&diff_base); + cx.set_head_text(&diff_base); cx.update_editor(|editor, window, cx| { editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx); }); @@ -14978,6 +14978,80 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp ); } +#[gpui::test] +async fn test_partially_staged_hunk(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_head_text(indoc! { " + one + two + three + four + five + " + }); + cx.set_index_text(indoc! { " + one + two + three + four + five + " + }); + cx.set_state(indoc! {" + one + TWO + ˇTHREE + FOUR + five + "}); + cx.run_until_parked(); + cx.update_editor(|editor, window, cx| { + editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx); + }); + cx.run_until_parked(); + cx.assert_index_text(Some(indoc! {" + one + TWO + THREE + FOUR + five + "})); + cx.set_state(indoc! { " + one + TWO + ˇTHREE-HUNDRED + FOUR + five + "}); + cx.run_until_parked(); + cx.update_editor(|editor, window, cx| { + let snapshot = editor.snapshot(window, cx); + let hunks = editor + .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot) + .collect::>(); + assert_eq!(hunks.len(), 1); + assert_eq!( + hunks[0].status(), + DiffHunkStatus { + kind: DiffHunkStatusKind::Modified, + secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk + } + ); + + editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx); + }); + cx.run_until_parked(); + cx.assert_index_text(Some(indoc! {" + one + TWO + THREE-HUNDRED + FOUR + five + "})); +} + #[gpui::test] fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -16341,7 +16415,7 @@ fn assert_hunk_revert( cx: &mut EditorLspTestContext, ) { cx.set_state(not_reverted_text_with_selections); - cx.set_diff_base(base_text); + cx.set_head_text(base_text); cx.executor().run_until_parked(); let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| { diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 1ace560e57056a..6a08f6e283ca5e 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -285,7 +285,7 @@ impl EditorTestContext { snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end) } - pub fn set_diff_base(&mut self, diff_base: &str) { + pub fn set_head_text(&mut self, diff_base: &str) { self.cx.run_until_parked(); let fs = self.update_editor(|editor, _, cx| { editor.project.as_ref().unwrap().read(cx).fs().as_fake() @@ -298,6 +298,19 @@ impl EditorTestContext { self.cx.run_until_parked(); } + pub fn set_index_text(&mut self, diff_base: &str) { + self.cx.run_until_parked(); + let fs = self.update_editor(|editor, _, cx| { + editor.project.as_ref().unwrap().read(cx).fs().as_fake() + }); + let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone()); + fs.set_index_for_repo( + &Self::root_path().join(".git"), + &[(path.into(), diff_base.to_string())], + ); + self.cx.run_until_parked(); + } + #[track_caller] pub fn assert_index_text(&mut self, expected: Option<&str>) { let fs = self.update_editor(|editor, _, cx| { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 89a6f5d7d6d818..9ff81458e1610f 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -131,7 +131,6 @@ pub struct MultiBufferDiffHunk { pub diff_base_byte_range: Range, /// Whether or not this hunk also appears in the 'secondary diff'. pub secondary_status: DiffHunkSecondaryStatus, - pub secondary_diff_base_byte_range: Option>, } impl MultiBufferDiffHunk { @@ -3506,7 +3505,6 @@ impl MultiBufferSnapshot { buffer_range: hunk.buffer_range.clone(), diff_base_byte_range: hunk.diff_base_byte_range.clone(), secondary_status: hunk.secondary_status, - secondary_diff_base_byte_range: hunk.secondary_diff_base_byte_range, }) }) } @@ -3876,7 +3874,6 @@ impl MultiBufferSnapshot { buffer_range: hunk.buffer_range.clone(), diff_base_byte_range: hunk.diff_base_byte_range.clone(), secondary_status: hunk.secondary_status, - secondary_diff_base_byte_range: hunk.secondary_diff_base_byte_range, }); } } diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 55d2b2b62398b2..a0d0f8df177062 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -2934,6 +2934,7 @@ impl ToOffset for Point { } impl ToOffset for usize { + #[track_caller] fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { assert!( *self <= snapshot.len(),