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
5use rope::Rope;5use rope::Rope;
6use std::{cmp, future::Future, iter, ops::Range, sync::Arc};6use std::{cmp, future::Future, iter, ops::Range, sync::Arc};
7use sum_tree::SumTree;7use sum_tree::SumTree;
8use text::ToOffset as _;
8use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};9use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
9use util::ResultExt;10use util::ResultExt;
10 11
@@ -14,10 +15,11 @@ pub struct BufferDiff { · 3 unmodified lines
14 secondary_diff: Option<Entity<BufferDiff>>,15 secondary_diff: Option<Entity<BufferDiff>>,
15}16}
16 17
17#[derive(Clone)]18#[derive(Clone, Debug)]
18pub struct BufferDiffSnapshot {19pub struct BufferDiffSnapshot {
19 inner: BufferDiffInner,20 inner: BufferDiffInner,
20 secondary_diff: Option<Box<BufferDiffSnapshot>>,21 secondary_diff: Option<Box<BufferDiffSnapshot>>,
22 pub is_single_insertion: bool,
21}23}
22 24
23#[derive(Clone)]25#[derive(Clone)]
@@ -40,21 +42,6 @@ pub enum DiffHunkSecondaryStatus { · 16 unmodified lines
40 None,42 None,
41}43}
42 44
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
58/// A diff hunk resolved to rows in the buffer.45/// A diff hunk resolved to rows in the buffer.
59#[derive(Debug, Clone, PartialEq, Eq)]46#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct DiffHunk {47pub struct DiffHunk {
@@ -65,6 +52,7 @@ pub struct DiffHunk { · 4 unmodified lines
65 /// The range in the buffer's diff base text to which this hunk corresponds.52 /// The range in the buffer's diff base text to which this hunk corresponds.
66 pub diff_base_byte_range: Range<usize>,53 pub diff_base_byte_range: Range<usize>,
67 pub secondary_status: DiffHunkSecondaryStatus,54 pub secondary_status: DiffHunkSecondaryStatus,
55 pub secondary_diff_base_byte_range: Option<Range<usize>>,
68}56}
69 57
70/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.58/// 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
166 }154 }
167 }155 }
168 }156 }
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 }
169}250}
170 251
171impl BufferDiffInner {252impl BufferDiffInner {
@@ -225,6 +306,7 @@ impl BufferDiffInner { · 53 unmodified lines
225 }306 }
226 307
227 let mut secondary_status = DiffHunkSecondaryStatus::None;308 let mut secondary_status = DiffHunkSecondaryStatus::None;
309 let mut secondary_diff_base_byte_range = None;
228 if let Some(secondary_cursor) = secondary_cursor.as_mut() {310 if let Some(secondary_cursor) = secondary_cursor.as_mut() {
229 if start_anchor311 if start_anchor
230 .cmp(&secondary_cursor.start().buffer_range.start, buffer)312 .cmp(&secondary_cursor.start().buffer_range.start, buffer)
@@ -234,9 +316,15 @@ impl BufferDiffInner { · 3 unmodified lines
234 }316 }
235 317
236 if let Some(secondary_hunk) = secondary_cursor.item() {318 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 }
238 if secondary_range == (start_point..end_point) {324 if secondary_range == (start_point..end_point) {
239 secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;325 secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
326 secondary_diff_base_byte_range =
327 Some(secondary_hunk.diff_base_byte_range.clone());
240 } else if secondary_range.start <= end_point {328 } else if secondary_range.start <= end_point {
241 secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;329 secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
242 }330 }
@@ -248,6 +336,7 @@ impl BufferDiffInner { · 5 unmodified lines
248 diff_base_byte_range: start_base..end_base,336 diff_base_byte_range: start_base..end_base,
249 buffer_range: start_anchor..end_anchor,337 buffer_range: start_anchor..end_anchor,
250 secondary_status,338 secondary_status,
339 secondary_diff_base_byte_range,
251 });340 });
252 })341 })
253 }342 }
@@ -282,6 +371,7 @@ impl BufferDiffInner { · 28 unmodified lines
282 buffer_range: hunk.buffer_range.clone(),371 buffer_range: hunk.buffer_range.clone(),
283 // The secondary status is not used by callers of this method.372 // The secondary status is not used by callers of this method.
284 secondary_status: DiffHunkSecondaryStatus::None,373 secondary_status: DiffHunkSecondaryStatus::None,
374 secondary_diff_base_byte_range: None,
285 })375 })
286 })376 })
287 }377 }
@@ -351,12 +441,12 @@ impl BufferDiffInner { · 63 unmodified lines
351}441}
352 442
353fn compute_hunks(443fn compute_hunks(
354 diff_base: Option<Arc<String>>,444 diff_base: Option<(Arc<String>, Rope)>,
355 buffer: text::BufferSnapshot,445 buffer: text::BufferSnapshot,
356) -> SumTree<InternalDiffHunk> {446) -> SumTree<InternalDiffHunk> {
357 let mut tree = SumTree::new(&buffer);447 let mut tree = SumTree::new(&buffer);
358 448
359 if let Some(diff_base) = diff_base {449 if let Some((diff_base, diff_base_rope)) = diff_base {
360 let buffer_text = buffer.as_rope().to_string();450 let buffer_text = buffer.as_rope().to_string();
361 451
362 let mut options = GitOptions::default();452 let mut options = GitOptions::default();
@@ -387,7 +477,13 @@ fn compute_hunks( · 24 unmodified lines
387 if let Some(patch) = patch {477 if let Some(patch) = patch {
388 let mut divergence = 0;478 let mut divergence = 0;
389 for hunk_index in 0..patch.num_hunks() {479 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 );
391 tree.push(hunk, &buffer);487 tree.push(hunk, &buffer);
392 }488 }
393 }489 }
@@ -399,6 +495,7 @@ fn compute_hunks( · 5 unmodified lines
399fn process_patch_hunk(495fn process_patch_hunk(
400 patch: &GitPatch<'_>,496 patch: &GitPatch<'_>,
401 hunk_index: usize,497 hunk_index: usize,
498 diff_base: &Rope,
402 buffer: &text::BufferSnapshot,499 buffer: &text::BufferSnapshot,
403 buffer_row_divergence: &mut i64,500 buffer_row_divergence: &mut i64,
404) -> InternalDiffHunk {501) -> InternalDiffHunk {
@@ -408,50 +505,59 @@ fn process_patch_hunk( · 3 unmodified lines
408 let mut first_deletion_buffer_row: Option<u32> = None;505 let mut first_deletion_buffer_row: Option<u32> = None;
409 let mut buffer_row_range: Option<Range<u32>> = None;506 let mut buffer_row_range: Option<Range<u32>> = None;
410 let mut diff_base_byte_range: Option<Range<usize>> = None;507 let mut diff_base_byte_range: Option<Range<usize>> = None;
508 let mut first_addition_old_row: Option<u32> = None;
411 509
412 for line_index in 0..line_item_count {510 for line_index in 0..line_item_count {
413 let line = patch.line_in_hunk(hunk_index, line_index).unwrap();511 let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
414 let kind = line.origin_value();512 let kind = line.origin_value();
415 let content_offset = line.content_offset() as isize;513 let content_offset = line.content_offset() as isize;
416 let content_len = line.content().len() as isize;514 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);
417 524
418 if kind == GitDiffLineType::Addition {525 match &mut buffer_row_range {
419 *buffer_row_divergence += 1;526 Some(Range { end, .. }) => *end = row + 1,
420 let row = line.new_lineno().unwrap().saturating_sub(1);527 None => buffer_row_range = Some(row..row + 1),
421 528 }
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),
425 }529 }
426 }530 GitDiffLineType::Deletion => {
531 let end = content_offset + content_len;
427 532
428 if kind == GitDiffLineType::Deletion {533 match &mut diff_base_byte_range {
429 let end = content_offset + content_len;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 }
430 537
431 match &mut diff_base_byte_range {538 if first_deletion_buffer_row.is_none() {
432 Some(head_byte_range) => head_byte_range.end = end as usize,539 let old_row = line.old_lineno().unwrap().saturating_sub(1);
433 None => diff_base_byte_range = Some(content_offset as usize..end as usize),540 let row = old_row as i64 + *buffer_row_divergence;
434 }541 first_deletion_buffer_row = Some(row as u32);
542 }
435 543
436 if first_deletion_buffer_row.is_none() {544 *buffer_row_divergence -= 1;
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);
440 }545 }
441 546 _ => {}
442 *buffer_row_divergence -= 1;
443 }547 }
444 }548 }
445 549
446 //unwrap_or deletion without addition
447 let buffer_row_range = buffer_row_range.unwrap_or_else(|| {550 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 hunk551 // Pure deletion hunk without addition.
449 let row = first_deletion_buffer_row.unwrap();552 let row = first_deletion_buffer_row.unwrap();
450 row..row553 row..row
451 });554 });
452 555 let diff_base_byte_range = diff_base_byte_range.unwrap_or_else(|| {
453 //unwrap_or addition without deletion556 // Pure addition hunk without deletion.
454 let diff_base_byte_range = diff_base_byte_range.unwrap_or(0..0);557 let row = first_addition_old_row.unwrap();
558 let offset = diff_base.point_to_offset(Point::new(row, 0));
559 offset..offset
560 });
455 561
456 let start = Point::new(buffer_row_range.start, 0);562 let start = Point::new(buffer_row_range.start, 0);
457 let end = Point::new(buffer_row_range.end, 0);563 let end = Point::new(buffer_row_range.end, 0);
@@ -499,9 +605,11 @@ impl BufferDiff { · 41 unmodified lines
499 language_registry: Option<Arc<LanguageRegistry>>,605 language_registry: Option<Arc<LanguageRegistry>>,
500 cx: &mut App,606 cx: &mut App,
501 ) -> impl Future<Output = BufferDiffInner> {607 ) -> 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)| {
503 language::Buffer::build_snapshot(611 language::Buffer::build_snapshot(
504 Rope::from(base_text.as_str()),612 diff_base.clone(),
505 language.clone(),613 language.clone(),
506 language_registry.clone(),614 language_registry.clone(),
507 cx,615 cx,
@@ -528,6 +636,11 @@ impl BufferDiff { · 20 unmodified lines
528 diff_base_buffer: Option<language::BufferSnapshot>,636 diff_base_buffer: Option<language::BufferSnapshot>,
529 cx: &App,637 cx: &App,
530 ) -> impl Future<Output = BufferDiffInner> {638 ) -> 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 );
531 cx.background_executor().spawn(async move {644 cx.background_executor().spawn(async move {
532 BufferDiffInner {645 BufferDiffInner {
533 hunks: compute_hunks(diff_base, buffer),646 hunks: compute_hunks(diff_base, buffer),
@@ -545,6 +658,7 @@ impl BufferDiff { · 11 unmodified lines
545 658
546 pub fn build_with_single_insertion(659 pub fn build_with_single_insertion(
547 insertion_present_in_secondary_diff: bool,660 insertion_present_in_secondary_diff: bool,
661 buffer: language::BufferSnapshot,
548 cx: &mut App,662 cx: &mut App,
549 ) -> BufferDiffSnapshot {663 ) -> BufferDiffSnapshot {
550 let base_text = language::Buffer::build_empty_snapshot(cx);664 let base_text = language::Buffer::build_empty_snapshot(cx);
@@ -560,17 +674,23 @@ impl BufferDiff { · 9 unmodified lines
560 hunks: hunks.clone(),674 hunks: hunks.clone(),
561 base_text: Some(base_text.clone()),675 base_text: Some(base_text.clone()),
562 },676 },
563 secondary_diff: if insertion_present_in_secondary_diff {677 secondary_diff: Some(Box::new(BufferDiffSnapshot {
564 Some(Box::new(BufferDiffSnapshot {678 inner: BufferDiffInner {
565 inner: BufferDiffInner {679 hunks: if insertion_present_in_secondary_diff {
566 hunks,680 hunks
567 base_text: Some(base_text),681 } else {
682 SumTree::new(&buffer.text)
568 },683 },
569 secondary_diff: None,684 base_text: Some(if insertion_present_in_secondary_diff {
570 }))685 base_text
571 } else {686 } else {
572 None687 buffer
573 },688 }),
689 },
690 secondary_diff: None,
691 is_single_insertion: true,
692 })),
693 is_single_insertion: true,
574 }694 }
575 }695 }
576 696
@@ -675,6 +795,7 @@ impl BufferDiff { · 98 unmodified lines
675 .secondary_diff795 .secondary_diff
676 .as_ref()796 .as_ref()
677 .map(|diff| Box::new(diff.read(cx).snapshot(cx))),797 .map(|diff| Box::new(diff.read(cx).snapshot(cx))),
798 is_single_insertion: false,
678 }799 }
679 }800 }
680 801
@@ -875,13 +996,21 @@ pub fn assert_hunks<Iter>( · 194 unmodified lines
875 996
876#[cfg(test)]997#[cfg(test)]
877mod tests {998mod tests {
878 use std::assert_eq;999 use std::fmt::Write as _;
879 1000
880 use super::*;1001 use super::*;
881 use gpui::TestAppContext;1002 use gpui::{AppContext as _, TestAppContext};
882 use text::{Buffer, BufferId};1003 use rand::{rngs::StdRng, Rng as _};
1004 use text::{Buffer, BufferId, Rope};
883 use unindent::Unindent as _;1005 use unindent::Unindent as _;
884 1006
1007 #[ctor::ctor]
1008 fn init_logger() {
1009 if std::env::var("RUST_LOG").is_ok() {
1010 env_logger::init();
1011 }
1012 }
1013
885 #[gpui::test]1014 #[gpui::test]
886 async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {1015 async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
887 let diff_base = "1016 let diff_base = "
@@ -1200,4 +1329,192 @@ mod tests { · 312 unmodified lines
1200 let range = diff_6.compare(&diff_5, &buffer).unwrap();1329 let range = diff_6.compare(&diff_5, &buffer).unwrap();
1201 assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));1330 assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
1202 }1331 }
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 }
1203}1520}