History

Implement staging and unstaging hunks (#24606)

- [x] Staging hunks - [x] Unstaging hunks - [x] Write a randomized test - [x] Get test passing - [x] Fix existing bug in diff_base_byte_range computation - [x] Remote project support - [ ] ~~Improve performance of buffer_range_to_unchanged_diff_base_range~~ - [ ] ~~Bug: project diff editor scrolls to top when staging/unstaging hunk~~ existing issue - [ ] ~~UI~~ deferred - [x] Tricky cases - [x] Correctly handle acting on multiple hunks for a single file - [x] Remove path from index when unstaging the last staged hunk, if it's absent from HEAD, or staging the only hunk, if it's deleted in the working copy Release Notes: - Add `ToggleStagedSelectedDiffHunks` action for staging and unstaging individual diff hunks

Cole Miller committed 1 year ago

Commit eea6b521 parent ea8da4318 files · +767 −69Browse files

Showing the changes to crates/buffer_diff/src/buffer_diff.rs (1 of 18 files). Show all

Unified · Split

crates/buffer_diff/src/buffer_diff.rs+377−60
@@ -5,6 +5,7 @@ use language::{Language, LanguageRegistry}; · 4 unmodified lines
55 use rope::Rope;
66 use std::{cmp, future::Future, iter, ops::Range, sync::Arc};
77 use sum_tree::SumTree;
8+use text::ToOffset as _;
89 use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
910 use util::ResultExt;
1011
@@ -14,10 +15,11 @@ pub struct BufferDiff { · 3 unmodified lines
1415 secondary_diff: Option<Entity<BufferDiff>>,
1516 }
1617
17−#[derive(Clone)]
18+#[derive(Clone, Debug)]
1819 pub struct BufferDiffSnapshot {
1920 inner: BufferDiffInner,
2021 secondary_diff: Option<Box<BufferDiffSnapshot>>,
22+ pub is_single_insertion: bool,
2123 }
2224
2325 #[derive(Clone)]
@@ -40,21 +42,6 @@ pub enum DiffHunkSecondaryStatus { · 16 unmodified lines
4042 None,
4143 }
4244
43−// to stage a hunk:
44−// - assume hunk starts out as not staged
45−// - hunk exists with the same buffer range in the unstaged diff and the uncommitted diff
46−// - we want to construct a "version" of the file that
47−// - starts from the index base text
48−// - has the single hunk applied to it
49−// - the hunk is the one from the UNSTAGED diff, so that the diff base offset range is correct to apply to that diff base
50−// - write that new version of the file into the index
51−
52−// to unstage a hunk
53−// - no hunk in the unstaged diff intersects this hunk from the uncommitted diff
54−// - we want to compute the hunk that
55−// - we can apply to the index text
56−// - at the end of applying it,
57−
5845 /// A diff hunk resolved to rows in the buffer.
5946 #[derive(Debug, Clone, PartialEq, Eq)]
6047 pub struct DiffHunk {
@@ -65,6 +52,7 @@ pub struct DiffHunk { · 4 unmodified lines
6552 /// The range in the buffer's diff base text to which this hunk corresponds.
6653 pub diff_base_byte_range: Range<usize>,
6754 pub secondary_status: DiffHunkSecondaryStatus,
55+ pub secondary_diff_base_byte_range: Option<Range<usize>>,
6856 }
6957
7058 /// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
@@ -166,6 +154,99 @@ impl BufferDiffSnapshot { · 95 unmodified lines
166154 }
167155 }
168156 }
157+
158+ fn buffer_range_to_unchanged_diff_base_range(
159+ &self,
160+ buffer_range: Range<Anchor>,
161+ buffer: &text::BufferSnapshot,
162+ ) -> Option<Range<usize>> {
163+ let mut hunks = self.inner.hunks.iter();
164+ let mut start = 0;
165+ let mut pos = buffer.anchor_before(0);
166+ while let Some(hunk) = hunks.next() {
167+ assert!(buffer_range.start.cmp(&pos, buffer).is_ge());
168+ assert!(hunk.buffer_range.start.cmp(&pos, buffer).is_ge());
169+ if hunk
170+ .buffer_range
171+ .start
172+ .cmp(&buffer_range.end, buffer)
173+ .is_ge()
174+ {
175+ // target buffer range is contained in the unchanged stretch leading up to this next hunk,
176+ // so do a final adjustment based on that
177+ break;
178+ }
179+
180+ // if the target buffer range intersects this hunk at all, no dice
181+ if buffer_range
182+ .start
183+ .cmp(&hunk.buffer_range.end, buffer)
184+ .is_lt()
185+ {
186+ return None;
187+ }
188+
189+ start += hunk.buffer_range.start.to_offset(buffer) - pos.to_offset(buffer);
190+ start += hunk.diff_base_byte_range.end - hunk.diff_base_byte_range.start;
191+ pos = hunk.buffer_range.end;
192+ }
193+ start += buffer_range.start.to_offset(buffer) - pos.to_offset(buffer);
194+ let end = start + buffer_range.end.to_offset(buffer) - buffer_range.start.to_offset(buffer);
195+ Some(start..end)
196+ }
197+
198+ pub fn secondary_edits_for_stage_or_unstage(
199+ &self,
200+ stage: bool,
201+ hunks: impl Iterator<Item = (Range<usize>, Option<Range<usize>>, Range<Anchor>)>,
202+ buffer: &text::BufferSnapshot,
203+ ) -> Vec<(Range<usize>, String)> {
204+ let Some(secondary_diff) = self.secondary_diff() else {
205+ log::debug!("no secondary diff");
206+ return Vec::new();
207+ };
208+ let index_base = secondary_diff.base_text().map_or_else(
209+ || Rope::from(""),
210+ |snapshot| snapshot.text.as_rope().clone(),
211+ );
212+ let head_base = self.base_text().map_or_else(
213+ || Rope::from(""),
214+ |snapshot| snapshot.text.as_rope().clone(),
215+ );
216+ log::debug!("original: {:?}", index_base.to_string());
217+ let mut edits = Vec::new();
218+ for (diff_base_byte_range, secondary_diff_base_byte_range, buffer_range) in hunks {
219+ let (index_byte_range, replacement_text) = if stage {
220+ log::debug!("staging");
221+ let mut replacement_text = String::new();
222+ let Some(index_byte_range) = secondary_diff_base_byte_range.clone() else {
223+ log::debug!("not a stageable hunk");
224+ continue;
225+ };
226+ log::debug!("using {:?}", index_byte_range);
227+ for chunk in buffer.text_for_range(buffer_range.clone()) {
228+ replacement_text.push_str(chunk);
229+ }
230+ (index_byte_range, replacement_text)
231+ } else {
232+ log::debug!("unstaging");
233+ let mut replacement_text = String::new();
234+ let Some(index_byte_range) = secondary_diff
235+ .buffer_range_to_unchanged_diff_base_range(buffer_range.clone(), &buffer)
236+ else {
237+ log::debug!("not an unstageable hunk");
238+ continue;
239+ };
240+ for chunk in head_base.chunks_in_range(diff_base_byte_range.clone()) {
241+ replacement_text.push_str(chunk);
242+ }
243+ (index_byte_range, replacement_text)
244+ };
245+ edits.push((index_byte_range, replacement_text));
246+ }
247+ log::debug!("edits: {edits:?}");
248+ edits
249+ }
169250 }
170251
171252 impl BufferDiffInner {
@@ -225,6 +306,7 @@ impl BufferDiffInner { · 53 unmodified lines
225306 }
226307
227308 let mut secondary_status = DiffHunkSecondaryStatus::None;
309+ let mut secondary_diff_base_byte_range = None;
228310 if let Some(secondary_cursor) = secondary_cursor.as_mut() {
229311 if start_anchor
230312 .cmp(&secondary_cursor.start().buffer_range.start, buffer)
@@ -234,9 +316,15 @@ impl BufferDiffInner { · 3 unmodified lines
234316 }
235317
236318 if let Some(secondary_hunk) = secondary_cursor.item() {
237− let secondary_range = secondary_hunk.buffer_range.to_point(buffer);
319+ let mut secondary_range = secondary_hunk.buffer_range.to_point(buffer);
320+ if secondary_range.end.column > 0 {
321+ secondary_range.end.row += 1;
322+ secondary_range.end.column = 0;
323+ }
238324 if secondary_range == (start_point..end_point) {
239325 secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
326+ secondary_diff_base_byte_range =
327+ Some(secondary_hunk.diff_base_byte_range.clone());
240328 } else if secondary_range.start <= end_point {
241329 secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
242330 }
@@ -248,6 +336,7 @@ impl BufferDiffInner { · 5 unmodified lines
248336 diff_base_byte_range: start_base..end_base,
249337 buffer_range: start_anchor..end_anchor,
250338 secondary_status,
339+ secondary_diff_base_byte_range,
251340 });
252341 })
253342 }
@@ -282,6 +371,7 @@ impl BufferDiffInner { · 28 unmodified lines
282371 buffer_range: hunk.buffer_range.clone(),
283372 // The secondary status is not used by callers of this method.
284373 secondary_status: DiffHunkSecondaryStatus::None,
374+ secondary_diff_base_byte_range: None,
285375 })
286376 })
287377 }
@@ -351,12 +441,12 @@ impl BufferDiffInner { · 63 unmodified lines
351441 }
352442
353443 fn compute_hunks(
354− diff_base: Option<Arc<String>>,
444+ diff_base: Option<(Arc<String>, Rope)>,
355445 buffer: text::BufferSnapshot,
356446 ) -> SumTree<InternalDiffHunk> {
357447 let mut tree = SumTree::new(&buffer);
358448
359− if let Some(diff_base) = diff_base {
449+ if let Some((diff_base, diff_base_rope)) = diff_base {
360450 let buffer_text = buffer.as_rope().to_string();
361451
362452 let mut options = GitOptions::default();
@@ -387,7 +477,13 @@ fn compute_hunks( · 24 unmodified lines
387477 if let Some(patch) = patch {
388478 let mut divergence = 0;
389479 for hunk_index in 0..patch.num_hunks() {
390− let hunk = process_patch_hunk(&patch, hunk_index, &buffer, &mut divergence);
480+ let hunk = process_patch_hunk(
481+ &patch,
482+ hunk_index,
483+ &diff_base_rope,
484+ &buffer,
485+ &mut divergence,
486+ );
391487 tree.push(hunk, &buffer);
392488 }
393489 }
@@ -399,6 +495,7 @@ fn compute_hunks( · 5 unmodified lines
399495 fn process_patch_hunk(
400496 patch: &GitPatch<'_>,
401497 hunk_index: usize,
498+ diff_base: &Rope,
402499 buffer: &text::BufferSnapshot,
403500 buffer_row_divergence: &mut i64,
404501 ) -> InternalDiffHunk {
@@ -408,50 +505,59 @@ fn process_patch_hunk( · 3 unmodified lines
408505 let mut first_deletion_buffer_row: Option<u32> = None;
409506 let mut buffer_row_range: Option<Range<u32>> = None;
410507 let mut diff_base_byte_range: Option<Range<usize>> = None;
508+ let mut first_addition_old_row: Option<u32> = None;
411509
412510 for line_index in 0..line_item_count {
413511 let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
414512 let kind = line.origin_value();
415513 let content_offset = line.content_offset() as isize;
416514 let content_len = line.content().len() as isize;
515+ match kind {
516+ GitDiffLineType::Addition => {
517+ if first_addition_old_row.is_none() {
518+ first_addition_old_row = Some(
519+ (line.new_lineno().unwrap() as i64 - *buffer_row_divergence - 1) as u32,
520+ );
521+ }
522+ *buffer_row_divergence += 1;
523+ let row = line.new_lineno().unwrap().saturating_sub(1);
417524
418− if kind == GitDiffLineType::Addition {
419− *buffer_row_divergence += 1;
420− let row = line.new_lineno().unwrap().saturating_sub(1);
421−
422− match &mut buffer_row_range {
423− Some(buffer_row_range) => buffer_row_range.end = row + 1,
424− None => buffer_row_range = Some(row..row + 1),
525+ match &mut buffer_row_range {
526+ Some(Range { end, .. }) => *end = row + 1,
527+ None => buffer_row_range = Some(row..row + 1),
528+ }
425529 }
426− }
530+ GitDiffLineType::Deletion => {
531+ let end = content_offset + content_len;
427532
428− if kind == GitDiffLineType::Deletion {
429− let end = content_offset + content_len;
533+ match &mut diff_base_byte_range {
534+ Some(head_byte_range) => head_byte_range.end = end as usize,
535+ None => diff_base_byte_range = Some(content_offset as usize..end as usize),
536+ }
430537
431− match &mut diff_base_byte_range {
432− Some(head_byte_range) => head_byte_range.end = end as usize,
433− None => diff_base_byte_range = Some(content_offset as usize..end as usize),
434− }
538+ if first_deletion_buffer_row.is_none() {
539+ let old_row = line.old_lineno().unwrap().saturating_sub(1);
540+ let row = old_row as i64 + *buffer_row_divergence;
541+ first_deletion_buffer_row = Some(row as u32);
542+ }
435543
436− if first_deletion_buffer_row.is_none() {
437− let old_row = line.old_lineno().unwrap().saturating_sub(1);
438− let row = old_row as i64 + *buffer_row_divergence;
439− first_deletion_buffer_row = Some(row as u32);
544+ *buffer_row_divergence -= 1;
440545 }
441−
442− *buffer_row_divergence -= 1;
546+ _ => {}
443547 }
444548 }
445549
446− //unwrap_or deletion without addition
447550 let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
448− //we cannot have an addition-less hunk without deletion(s) or else there would be no hunk
551+ // Pure deletion hunk without addition.
449552 let row = first_deletion_buffer_row.unwrap();
450553 row..row
451554 });
452−
453− //unwrap_or addition without deletion
454− let diff_base_byte_range = diff_base_byte_range.unwrap_or(0..0);
555+ let diff_base_byte_range = diff_base_byte_range.unwrap_or_else(|| {
556+ // Pure addition hunk without deletion.
557+ let row = first_addition_old_row.unwrap();
558+ let offset = diff_base.point_to_offset(Point::new(row, 0));
559+ offset..offset
560+ });
455561
456562 let start = Point::new(buffer_row_range.start, 0);
457563 let end = Point::new(buffer_row_range.end, 0);
@@ -499,9 +605,11 @@ impl BufferDiff { · 41 unmodified lines
499605 language_registry: Option<Arc<LanguageRegistry>>,
500606 cx: &mut App,
501607 ) -> impl Future<Output = BufferDiffInner> {
502− let base_text_snapshot = diff_base.as_ref().map(|base_text| {
608+ let diff_base =
609+ diff_base.map(|diff_base| (diff_base.clone(), Rope::from(diff_base.as_str())));
610+ let base_text_snapshot = diff_base.as_ref().map(|(_, diff_base)| {
503611 language::Buffer::build_snapshot(
504− Rope::from(base_text.as_str()),
612+ diff_base.clone(),
505613 language.clone(),
506614 language_registry.clone(),
507615 cx,
@@ -528,6 +636,11 @@ impl BufferDiff { · 20 unmodified lines
528636 diff_base_buffer: Option<language::BufferSnapshot>,
529637 cx: &App,
530638 ) -> impl Future<Output = BufferDiffInner> {
639+ let diff_base = diff_base.clone().zip(
640+ diff_base_buffer
641+ .clone()
642+ .map(|buffer| buffer.as_rope().clone()),
643+ );
531644 cx.background_executor().spawn(async move {
532645 BufferDiffInner {
533646 hunks: compute_hunks(diff_base, buffer),
@@ -545,6 +658,7 @@ impl BufferDiff { · 11 unmodified lines
545658
546659 pub fn build_with_single_insertion(
547660 insertion_present_in_secondary_diff: bool,
661+ buffer: language::BufferSnapshot,
548662 cx: &mut App,
549663 ) -> BufferDiffSnapshot {
550664 let base_text = language::Buffer::build_empty_snapshot(cx);
@@ -560,17 +674,23 @@ impl BufferDiff { · 9 unmodified lines
560674 hunks: hunks.clone(),
561675 base_text: Some(base_text.clone()),
562676 },
563− secondary_diff: if insertion_present_in_secondary_diff {
564− Some(Box::new(BufferDiffSnapshot {
565− inner: BufferDiffInner {
566− hunks,
567− base_text: Some(base_text),
677+ secondary_diff: Some(Box::new(BufferDiffSnapshot {
678+ inner: BufferDiffInner {
679+ hunks: if insertion_present_in_secondary_diff {
680+ hunks
681+ } else {
682+ SumTree::new(&buffer.text)
568683 },
569− secondary_diff: None,
570− }))
571− } else {
572− None
573− },
684+ base_text: Some(if insertion_present_in_secondary_diff {
685+ base_text
686+ } else {
687+ buffer
688+ }),
689+ },
690+ secondary_diff: None,
691+ is_single_insertion: true,
692+ })),
693+ is_single_insertion: true,
574694 }
575695 }
576696
@@ -675,6 +795,7 @@ impl BufferDiff { · 98 unmodified lines
675795 .secondary_diff
676796 .as_ref()
677797 .map(|diff| Box::new(diff.read(cx).snapshot(cx))),
798+ is_single_insertion: false,
678799 }
679800 }
680801
@@ -875,13 +996,21 @@ pub fn assert_hunks<Iter>( · 194 unmodified lines
875996
876997 #[cfg(test)]
877998 mod tests {
878− use std::assert_eq;
999+ use std::fmt::Write as _;
8791000
8801001 use super::*;
881− use gpui::TestAppContext;
882− use text::{Buffer, BufferId};
1002+ use gpui::{AppContext as _, TestAppContext};
1003+ use rand::{rngs::StdRng, Rng as _};
1004+ use text::{Buffer, BufferId, Rope};
8831005 use unindent::Unindent as _;
8841006
1007+ #[ctor::ctor]
1008+ fn init_logger() {
1009+ if std::env::var("RUST_LOG").is_ok() {
1010+ env_logger::init();
1011+ }
1012+ }
1013+
8851014 #[gpui::test]
8861015 async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
8871016 let diff_base = "
@@ -1200,4 +1329,192 @@ mod tests { · 312 unmodified lines
12001329 let range = diff_6.compare(&diff_5, &buffer).unwrap();
12011330 assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
12021331 }
1332+
1333+ #[gpui::test(iterations = 100)]
1334+ async fn test_secondary_edits_for_stage_unstage(cx: &mut TestAppContext, mut rng: StdRng) {
1335+ fn gen_line(rng: &mut StdRng) -> String {
1336+ if rng.gen_bool(0.2) {
1337+ "\n".to_owned()
1338+ } else {
1339+ let c = rng.gen_range('A'..='Z');
1340+ format!("{c}{c}{c}\n")
1341+ }
1342+ }
1343+
1344+ fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
1345+ let mut old_lines = {
1346+ let mut old_lines = Vec::new();
1347+ let mut old_lines_iter = head.lines();
1348+ while let Some(line) = old_lines_iter.next() {
1349+ assert!(!line.ends_with("\n"));
1350+ old_lines.push(line.to_owned());
1351+ }
1352+ if old_lines.last().is_some_and(|line| line.is_empty()) {
1353+ old_lines.pop();
1354+ }
1355+ old_lines.into_iter()
1356+ };
1357+ let mut result = String::new();
1358+ let unchanged_count = rng.gen_range(0..=old_lines.len());
1359+ result +=
1360+ &old_lines
1361+ .by_ref()
1362+ .take(unchanged_count)
1363+ .fold(String::new(), |mut s, line| {
1364+ writeln!(&mut s, "{line}").unwrap();
1365+ s
1366+ });
1367+ while old_lines.len() > 0 {
1368+ let deleted_count = rng.gen_range(0..=old_lines.len());
1369+ let _advance = old_lines
1370+ .by_ref()
1371+ .take(deleted_count)
1372+ .map(|line| line.len() + 1)
1373+ .sum::<usize>();
1374+ let minimum_added = if deleted_count == 0 { 1 } else { 0 };
1375+ let added_count = rng.gen_range(minimum_added..=5);
1376+ let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
1377+ result += &addition;
1378+
1379+ if old_lines.len() > 0 {
1380+ let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
1381+ if blank_lines == old_lines.len() {
1382+ break;
1383+ };
1384+ let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
1385+ result += &old_lines.by_ref().take(unchanged_count).fold(
1386+ String::new(),
1387+ |mut s, line| {
1388+ writeln!(&mut s, "{line}").unwrap();
1389+ s
1390+ },
1391+ );
1392+ }
1393+ }
1394+ result
1395+ }
1396+
1397+ fn uncommitted_diff(
1398+ working_copy: &language::BufferSnapshot,
1399+ index_text: &Entity<language::Buffer>,
1400+ head_text: String,
1401+ cx: &mut TestAppContext,
1402+ ) -> BufferDiff {
1403+ let inner = BufferDiff::build_sync(working_copy.text.clone(), head_text, cx);
1404+ let secondary = BufferDiff {
1405+ buffer_id: working_copy.remote_id(),
1406+ inner: BufferDiff::build_sync(
1407+ working_copy.text.clone(),
1408+ index_text.read_with(cx, |index_text, _| index_text.text()),
1409+ cx,
1410+ ),
1411+ secondary_diff: None,
1412+ };
1413+ let secondary = cx.new(|_| secondary);
1414+ BufferDiff {
1415+ buffer_id: working_copy.remote_id(),
1416+ inner,
1417+ secondary_diff: Some(secondary),
1418+ }
1419+ }
1420+
1421+ let operations = std::env::var("OPERATIONS")
1422+ .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1423+ .unwrap_or(10);
1424+
1425+ let rng = &mut rng;
1426+ let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
1427+ writeln!(&mut s, "{c}{c}{c}").unwrap();
1428+ s
1429+ });
1430+ let working_copy = gen_working_copy(rng, &head_text);
1431+ let working_copy = cx.new(|cx| {
1432+ language::Buffer::local_normalized(
1433+ Rope::from(working_copy.as_str()),
1434+ text::LineEnding::default(),
1435+ cx,
1436+ )
1437+ });
1438+ let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
1439+ let index_text = cx.new(|cx| {
1440+ language::Buffer::local_normalized(
1441+ if rng.gen() {
1442+ Rope::from(head_text.as_str())
1443+ } else {
1444+ working_copy.as_rope().clone()
1445+ },
1446+ text::LineEnding::default(),
1447+ cx,
1448+ )
1449+ });
1450+
1451+ let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
1452+ let mut hunks = cx.update(|cx| {
1453+ diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
1454+ .collect::<Vec<_>>()
1455+ });
1456+ if hunks.len() == 0 {
1457+ return;
1458+ }
1459+
1460+ for _ in 0..operations {
1461+ let i = rng.gen_range(0..hunks.len());
1462+ let hunk = &mut hunks[i];
1463+ let hunk_fields = (
1464+ hunk.diff_base_byte_range.clone(),
1465+ hunk.secondary_diff_base_byte_range.clone(),
1466+ hunk.buffer_range.clone(),
1467+ );
1468+ let stage = match (
1469+ hunk.secondary_status,
1470+ hunk.secondary_diff_base_byte_range.clone(),
1471+ ) {
1472+ (DiffHunkSecondaryStatus::HasSecondaryHunk, Some(_)) => {
1473+ hunk.secondary_status = DiffHunkSecondaryStatus::None;
1474+ hunk.secondary_diff_base_byte_range = None;
1475+ true
1476+ }
1477+ (DiffHunkSecondaryStatus::None, None) => {
1478+ hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
1479+ // We don't look at this, just notice whether it's Some or not.
1480+ hunk.secondary_diff_base_byte_range = Some(17..17);
1481+ false
1482+ }
1483+ _ => unreachable!(),
1484+ };
1485+
1486+ let snapshot = cx.update(|cx| diff.snapshot(cx));
1487+ let edits = snapshot.secondary_edits_for_stage_or_unstage(
1488+ stage,
1489+ [hunk_fields].into_iter(),
1490+ &working_copy,
1491+ );
1492+ index_text.update(cx, |index_text, cx| {
1493+ index_text.edit(edits, None, cx);
1494+ });
1495+
1496+ diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
1497+ let found_hunks = cx.update(|cx| {
1498+ diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
1499+ .collect::<Vec<_>>()
1500+ });
1501+ assert_eq!(hunks.len(), found_hunks.len());
1502+ for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
1503+ assert_eq!(
1504+ expected_hunk.buffer_range.to_point(&working_copy),
1505+ found_hunk.buffer_range.to_point(&working_copy)
1506+ );
1507+ assert_eq!(
1508+ expected_hunk.diff_base_byte_range,
1509+ found_hunk.diff_base_byte_range
1510+ );
1511+ assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
1512+ assert_eq!(
1513+ expected_hunk.secondary_diff_base_byte_range.is_some(),
1514+ found_hunk.secondary_diff_base_byte_range.is_some()
1515+ )
1516+ }
1517+ hunks = found_hunks;
1518+ }
1519+ }
12031520 }