- [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
Showing the changes to crates/buffer_diff/src/buffer_diff.rs (1 of 18 files). Show all
Unified · Split
| @@ -5,6 +5,7 @@ use language::{Language, LanguageRegistry}; · 4 unmodified lines | ||
| 5 | 5 | use rope::Rope; |
| 6 | 6 | use std::{cmp, future::Future, iter, ops::Range, sync::Arc}; |
| 7 | 7 | use sum_tree::SumTree; |
| 8 | +use text::ToOffset as _; | |
| 8 | 9 | use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point}; |
| 9 | 10 | use util::ResultExt; |
| 10 | 11 | |
| @@ -14,10 +15,11 @@ pub struct BufferDiff { · 3 unmodified lines | ||
| 14 | 15 | secondary_diff: Option<Entity<BufferDiff>>, |
| 15 | 16 | } |
| 16 | 17 | |
| 17 | −#[derive(Clone)] | |
| 18 | +#[derive(Clone, Debug)] | |
| 18 | 19 | pub struct BufferDiffSnapshot { |
| 19 | 20 | inner: BufferDiffInner, |
| 20 | 21 | secondary_diff: Option<Box<BufferDiffSnapshot>>, |
| 22 | + pub is_single_insertion: bool, | |
| 21 | 23 | } |
| 22 | 24 | |
| 23 | 25 | #[derive(Clone)] |
| @@ -40,21 +42,6 @@ pub enum DiffHunkSecondaryStatus { · 16 unmodified lines | ||
| 40 | 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 | 45 | /// A diff hunk resolved to rows in the buffer. |
| 59 | 46 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 60 | 47 | pub struct DiffHunk { |
| @@ -65,6 +52,7 @@ pub struct DiffHunk { · 4 unmodified lines | ||
| 65 | 52 | /// The range in the buffer's diff base text to which this hunk corresponds. |
| 66 | 53 | pub diff_base_byte_range: Range<usize>, |
| 67 | 54 | pub secondary_status: DiffHunkSecondaryStatus, |
| 55 | + pub secondary_diff_base_byte_range: Option<Range<usize>>, | |
| 68 | 56 | } |
| 69 | 57 | |
| 70 | 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 | |
| 171 | 252 | impl BufferDiffInner { |
| @@ -225,6 +306,7 @@ impl BufferDiffInner { · 53 unmodified lines | ||
| 225 | 306 | } |
| 226 | 307 | |
| 227 | 308 | let mut secondary_status = DiffHunkSecondaryStatus::None; |
| 309 | + let mut secondary_diff_base_byte_range = None; | |
| 228 | 310 | if let Some(secondary_cursor) = secondary_cursor.as_mut() { |
| 229 | 311 | if start_anchor |
| 230 | 312 | .cmp(&secondary_cursor.start().buffer_range.start, buffer) |
| @@ -234,9 +316,15 @@ impl BufferDiffInner { · 3 unmodified lines | ||
| 234 | 316 | } |
| 235 | 317 | |
| 236 | 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 | 324 | if secondary_range == (start_point..end_point) { |
| 239 | 325 | secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk; |
| 326 | + secondary_diff_base_byte_range = | |
| 327 | + Some(secondary_hunk.diff_base_byte_range.clone()); | |
| 240 | 328 | } else if secondary_range.start <= end_point { |
| 241 | 329 | secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk; |
| 242 | 330 | } |
| @@ -248,6 +336,7 @@ impl BufferDiffInner { · 5 unmodified lines | ||
| 248 | 336 | diff_base_byte_range: start_base..end_base, |
| 249 | 337 | buffer_range: start_anchor..end_anchor, |
| 250 | 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 | 371 | buffer_range: hunk.buffer_range.clone(), |
| 283 | 372 | // The secondary status is not used by callers of this method. |
| 284 | 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 | |
| 353 | 443 | fn compute_hunks( |
| 354 | − diff_base: Option<Arc<String>>, | |
| 444 | + diff_base: Option<(Arc<String>, Rope)>, | |
| 355 | 445 | buffer: text::BufferSnapshot, |
| 356 | 446 | ) -> SumTree<InternalDiffHunk> { |
| 357 | 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 | 450 | let buffer_text = buffer.as_rope().to_string(); |
| 361 | 451 | |
| 362 | 452 | let mut options = GitOptions::default(); |
| @@ -387,7 +477,13 @@ fn compute_hunks( · 24 unmodified lines | ||
| 387 | 477 | if let Some(patch) = patch { |
| 388 | 478 | let mut divergence = 0; |
| 389 | 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 | 487 | tree.push(hunk, &buffer); |
| 392 | 488 | } |
| 393 | 489 | } |
| @@ -399,6 +495,7 @@ fn compute_hunks( · 5 unmodified lines | ||
| 399 | 495 | fn process_patch_hunk( |
| 400 | 496 | patch: &GitPatch<'_>, |
| 401 | 497 | hunk_index: usize, |
| 498 | + diff_base: &Rope, | |
| 402 | 499 | buffer: &text::BufferSnapshot, |
| 403 | 500 | buffer_row_divergence: &mut i64, |
| 404 | 501 | ) -> InternalDiffHunk { |
| @@ -408,50 +505,59 @@ fn process_patch_hunk( · 3 unmodified lines | ||
| 408 | 505 | let mut first_deletion_buffer_row: Option<u32> = None; |
| 409 | 506 | let mut buffer_row_range: Option<Range<u32>> = None; |
| 410 | 507 | let mut diff_base_byte_range: Option<Range<usize>> = None; |
| 508 | + let mut first_addition_old_row: Option<u32> = None; | |
| 411 | 509 | |
| 412 | 510 | for line_index in 0..line_item_count { |
| 413 | 511 | let line = patch.line_in_hunk(hunk_index, line_index).unwrap(); |
| 414 | 512 | let kind = line.origin_value(); |
| 415 | 513 | let content_offset = line.content_offset() as isize; |
| 416 | 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 { | |
| 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 | + } | |
| 425 | 529 | } |
| 426 | − } | |
| 530 | + GitDiffLineType::Deletion => { | |
| 531 | + let end = content_offset + content_len; | |
| 427 | 532 | |
| 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 | + } | |
| 430 | 537 | |
| 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 | + } | |
| 435 | 543 | |
| 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; | |
| 440 | 545 | } |
| 441 | − | |
| 442 | − *buffer_row_divergence -= 1; | |
| 546 | + _ => {} | |
| 443 | 547 | } |
| 444 | 548 | } |
| 445 | 549 | |
| 446 | − //unwrap_or deletion without addition | |
| 447 | 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 hunk | |
| 551 | + // Pure deletion hunk without addition. | |
| 449 | 552 | let row = first_deletion_buffer_row.unwrap(); |
| 450 | 553 | row..row |
| 451 | 554 | }); |
| 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 | + }); | |
| 455 | 561 | |
| 456 | 562 | let start = Point::new(buffer_row_range.start, 0); |
| 457 | 563 | let end = Point::new(buffer_row_range.end, 0); |
| @@ -499,9 +605,11 @@ impl BufferDiff { · 41 unmodified lines | ||
| 499 | 605 | language_registry: Option<Arc<LanguageRegistry>>, |
| 500 | 606 | cx: &mut App, |
| 501 | 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 | 611 | language::Buffer::build_snapshot( |
| 504 | − Rope::from(base_text.as_str()), | |
| 612 | + diff_base.clone(), | |
| 505 | 613 | language.clone(), |
| 506 | 614 | language_registry.clone(), |
| 507 | 615 | cx, |
| @@ -528,6 +636,11 @@ impl BufferDiff { · 20 unmodified lines | ||
| 528 | 636 | diff_base_buffer: Option<language::BufferSnapshot>, |
| 529 | 637 | cx: &App, |
| 530 | 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 | 644 | cx.background_executor().spawn(async move { |
| 532 | 645 | BufferDiffInner { |
| 533 | 646 | hunks: compute_hunks(diff_base, buffer), |
| @@ -545,6 +658,7 @@ impl BufferDiff { · 11 unmodified lines | ||
| 545 | 658 | |
| 546 | 659 | pub fn build_with_single_insertion( |
| 547 | 660 | insertion_present_in_secondary_diff: bool, |
| 661 | + buffer: language::BufferSnapshot, | |
| 548 | 662 | cx: &mut App, |
| 549 | 663 | ) -> BufferDiffSnapshot { |
| 550 | 664 | let base_text = language::Buffer::build_empty_snapshot(cx); |
| @@ -560,17 +674,23 @@ impl BufferDiff { · 9 unmodified lines | ||
| 560 | 674 | hunks: hunks.clone(), |
| 561 | 675 | base_text: Some(base_text.clone()), |
| 562 | 676 | }, |
| 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) | |
| 568 | 683 | }, |
| 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, | |
| 574 | 694 | } |
| 575 | 695 | } |
| 576 | 696 | |
| @@ -675,6 +795,7 @@ impl BufferDiff { · 98 unmodified lines | ||
| 675 | 795 | .secondary_diff |
| 676 | 796 | .as_ref() |
| 677 | 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 | 997 | #[cfg(test)] |
| 877 | 998 | mod tests { |
| 878 | − use std::assert_eq; | |
| 999 | + use std::fmt::Write as _; | |
| 879 | 1000 | |
| 880 | 1001 | 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}; | |
| 883 | 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 | 1014 | #[gpui::test] |
| 886 | 1015 | async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) { |
| 887 | 1016 | let diff_base = " |
| @@ -1200,4 +1329,192 @@ mod tests { · 312 unmodified lines | ||
| 1200 | 1329 | let range = diff_6.compare(&diff_5, &buffer).unwrap(); |
| 1201 | 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 | } |