- [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 | use rope::Rope; | 5 | use rope::Rope; |
| 6 | use std::{cmp, future::Future, iter, ops::Range, sync::Arc}; | 6 | use std::{cmp, future::Future, iter, ops::Range, sync::Arc}; |
| 7 | use sum_tree::SumTree; | 7 | use sum_tree::SumTree; |
| 8 | use text::ToOffset as _; | ||
| 8 | use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point}; | 9 | use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point}; |
| 9 | use util::ResultExt; | 10 | use 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)] |
| 18 | pub struct BufferDiffSnapshot { | 19 | pub 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)] |
| 60 | pub struct DiffHunk { | 47 | pub 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 | ||
| 171 | impl BufferDiffInner { | 252 | impl 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_anchor | 311 | 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 | ||
| 353 | fn compute_hunks( | 443 | fn 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 | |||
| 399 | fn process_patch_hunk( | 495 | fn 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 hunk | 551 | // Pure deletion hunk without addition. |
| 449 | let row = first_deletion_buffer_row.unwrap(); | 552 | let row = first_deletion_buffer_row.unwrap(); |
| 450 | row..row | 553 | row..row |
| 451 | }); | 554 | }); |
| 452 | 555 | let diff_base_byte_range = diff_base_byte_range.unwrap_or_else(|| { | |
| 453 | //unwrap_or addition without deletion | 556 | // 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 | None | 687 | 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_diff | 795 | .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)] |
| 877 | mod tests { | 998 | mod 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 | } |