Skip to repository content4327 lines · 152.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:22:27.473Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
buffer_diff.rs
1use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
2use imara_diff::{Algorithm, Diff, InternedInput, sources::lines};
3use language::{
4 Capability, DiffOptions, Language, LanguageName, LanguageRegistry,
5 language_settings::LanguageSettings, word_diff_ranges,
6};
7use rope::Rope;
8use std::{
9 cmp::Ordering,
10 iter,
11 ops::{Range, RangeInclusive},
12 sync::Arc,
13};
14use sum_tree::SumTree;
15use text::{
16 Anchor, Bias, BufferId, Edit, OffsetRangeExt, Patch, Point, ToOffset as _, ToPoint as _,
17};
18use util::{ResultExt, debug_panic};
19
20pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5;
21
22pub struct BufferDiff {
23 pub buffer_id: BufferId,
24 base_text_buffer: Entity<language::Buffer>,
25 diff_snapshot: Option<BufferDiffSnapshot>,
26 secondary_diff: Option<Entity<BufferDiff>>,
27 buffer_snapshot: text::BufferSnapshot,
28}
29
30#[derive(Clone)]
31pub struct BufferDiffSnapshot {
32 hunks: SumTree<InternalDiffHunk>,
33 pending_hunks: SumTree<PendingHunk>,
34 base_text: language::BufferSnapshot,
35 base_text_exists: bool,
36 buffer_snapshot: text::BufferSnapshot,
37 secondary_diff: Option<Arc<BufferDiffSnapshot>>,
38}
39
40impl std::fmt::Debug for BufferDiffSnapshot {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("BufferDiffSnapshot")
43 .field("hunks", &self.hunks)
44 .field("remote_id", &self.base_text.remote_id())
45 .field("secondary_diff", &self.secondary_diff)
46 .finish()
47 }
48}
49
50#[derive(Clone)]
51pub struct BufferDiffUpdate {
52 hunks: SumTree<InternalDiffHunk>,
53 base_text: language::BufferSnapshot,
54 base_text_exists: bool,
55 buffer_snapshot: text::BufferSnapshot,
56}
57
58impl BufferDiffUpdate {
59 pub fn set_base_text_snapshot(
60 &mut self,
61 base_text: language::BufferSnapshot,
62 base_text_exists: bool,
63 ) {
64 self.base_text = base_text;
65 self.base_text_exists = base_text_exists;
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub struct DiffHunkStatus {
71 pub kind: DiffHunkStatusKind,
72 pub secondary: DiffHunkSecondaryStatus,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub enum DiffHunkStatusKind {
77 Added,
78 Modified,
79 Deleted,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83/// Diff of Working Copy vs Index
84/// aka 'is this hunk staged or not'
85pub enum DiffHunkSecondaryStatus {
86 /// Unstaged
87 HasSecondaryHunk,
88 /// Partially staged
89 OverlapsWithSecondaryHunk,
90 /// Staged
91 NoSecondaryHunk,
92 /// We are unstaging
93 SecondaryHunkAdditionPending,
94 /// We are stagind
95 SecondaryHunkRemovalPending,
96}
97
98/// A diff hunk resolved to rows in the buffer.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct DiffHunk {
101 /// The buffer range as points.
102 pub range: Range<Point>,
103 /// The range in the buffer to which this hunk corresponds.
104 pub buffer_range: Range<Anchor>,
105 /// The range in the buffer's diff base text to which this hunk corresponds.
106 pub diff_base_byte_range: Range<usize>,
107 pub secondary_status: DiffHunkSecondaryStatus,
108 // Anchors representing the word diff locations in the active buffer
109 pub buffer_word_diffs: Vec<Range<Anchor>>,
110 // Offsets relative to the start of the deleted diff that represent word diff locations
111 pub base_word_diffs: Vec<Range<usize>>,
112}
113
114/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
115#[derive(Debug, Clone, PartialEq, Eq)]
116struct InternalDiffHunk {
117 buffer_range: Range<Anchor>,
118 diff_base_byte_range: Range<usize>,
119 diff_base_point_range: Range<Point>,
120 base_word_diffs: Vec<Range<usize>>,
121 buffer_word_diffs: Vec<Range<Anchor>>,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct PendingHunk {
126 buffer_range: Range<Anchor>,
127 diff_base_byte_range: Range<usize>,
128 buffer_version: clock::Global,
129 sense: PendingSense,
130}
131
132impl PendingHunk {
133 pub fn new(
134 buffer_range: Range<Anchor>,
135 diff_base_byte_range: Range<usize>,
136 buffer_version: clock::Global,
137 sense: PendingSense,
138 ) -> Self {
139 Self {
140 buffer_range,
141 diff_base_byte_range,
142 buffer_version,
143 sense,
144 }
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum PendingSense {
150 /// Override the secondary status of the matched hunk (used by the
151 /// uncommitted diff to show a hunk as staging/unstaging in place).
152 SetSecondaryStatus { stage: bool },
153 /// Suppress the matched hunk entirely (used by the unstaged/staged diffs so
154 /// that a hunk disappears the moment it is staged/unstaged).
155 Suppress,
156}
157
158#[derive(Debug, Clone)]
159pub struct DiffHunkSummary {
160 buffer_range: Range<Anchor>,
161 diff_base_byte_range: Range<usize>,
162 added_rows: u32,
163 removed_rows: u32,
164}
165
166impl sum_tree::Item for InternalDiffHunk {
167 type Summary = DiffHunkSummary;
168
169 fn summary(&self, buffer: &text::BufferSnapshot) -> Self::Summary {
170 let buffer_start = self.buffer_range.start.to_point(buffer);
171 let buffer_end = self.buffer_range.end.to_point(buffer);
172 DiffHunkSummary {
173 buffer_range: self.buffer_range.clone(),
174 diff_base_byte_range: self.diff_base_byte_range.clone(),
175 added_rows: buffer_end.row.saturating_sub(buffer_start.row),
176 removed_rows: self
177 .diff_base_point_range
178 .end
179 .row
180 .saturating_sub(self.diff_base_point_range.start.row),
181 }
182 }
183}
184
185impl sum_tree::Item for PendingHunk {
186 type Summary = DiffHunkSummary;
187
188 fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
189 DiffHunkSummary {
190 buffer_range: self.buffer_range.clone(),
191 diff_base_byte_range: self.diff_base_byte_range.clone(),
192 added_rows: 0,
193 removed_rows: 0,
194 }
195 }
196}
197
198impl sum_tree::Summary for DiffHunkSummary {
199 type Context<'a> = &'a text::BufferSnapshot;
200
201 fn zero(buffer: &text::BufferSnapshot) -> Self {
202 DiffHunkSummary {
203 buffer_range: Anchor::min_min_range_for_buffer(buffer.remote_id()),
204 diff_base_byte_range: 0..0,
205 added_rows: 0,
206 removed_rows: 0,
207 }
208 }
209
210 fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) {
211 self.buffer_range.start = *self
212 .buffer_range
213 .start
214 .min(&other.buffer_range.start, buffer);
215 self.buffer_range.end = *self.buffer_range.end.max(&other.buffer_range.end, buffer);
216
217 self.diff_base_byte_range.start = self
218 .diff_base_byte_range
219 .start
220 .min(other.diff_base_byte_range.start);
221 self.diff_base_byte_range.end = self
222 .diff_base_byte_range
223 .end
224 .max(other.diff_base_byte_range.end);
225
226 self.added_rows += other.added_rows;
227 self.removed_rows += other.removed_rows;
228 }
229}
230
231impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for Anchor {
232 fn cmp(&self, cursor_location: &DiffHunkSummary, buffer: &text::BufferSnapshot) -> Ordering {
233 if self
234 .cmp(&cursor_location.buffer_range.start, buffer)
235 .is_lt()
236 {
237 Ordering::Less
238 } else if self.cmp(&cursor_location.buffer_range.end, buffer).is_gt() {
239 Ordering::Greater
240 } else {
241 Ordering::Equal
242 }
243 }
244}
245
246impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for usize {
247 fn cmp(&self, cursor_location: &DiffHunkSummary, _cx: &text::BufferSnapshot) -> Ordering {
248 if *self < cursor_location.diff_base_byte_range.start {
249 Ordering::Less
250 } else if *self > cursor_location.diff_base_byte_range.end {
251 Ordering::Greater
252 } else {
253 Ordering::Equal
254 }
255 }
256}
257
258impl BufferDiffSnapshot {
259 #[cfg(test)]
260 fn new_sync(
261 buffer: &text::BufferSnapshot,
262 diff_base: String,
263 cx: &mut gpui::TestAppContext,
264 ) -> BufferDiffSnapshot {
265 let buffer_diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, buffer, cx));
266 buffer_diff.update(cx, |buffer_diff, cx| buffer_diff.snapshot(cx))
267 }
268
269 pub fn buffer_id(&self) -> BufferId {
270 self.buffer_snapshot.remote_id()
271 }
272
273 pub fn buffer_snapshot(&self) -> &text::BufferSnapshot {
274 &self.buffer_snapshot
275 }
276
277 pub fn is_empty(&self) -> bool {
278 self.hunks.is_empty()
279 }
280
281 pub fn changed_row_counts(&self) -> (u32, u32) {
282 let summary = self.hunks.summary();
283 (summary.added_rows, summary.removed_rows)
284 }
285
286 pub fn base_text_string(&self) -> Option<String> {
287 self.base_text_exists.then(|| self.base_text.text())
288 }
289
290 pub fn base_text_exists(&self) -> bool {
291 self.base_text_exists
292 }
293
294 pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> {
295 self.secondary_diff.as_deref()
296 }
297
298 pub fn buffer_version(&self) -> &clock::Global {
299 self.buffer_snapshot.version()
300 }
301
302 fn original_buffer_snapshot(&self) -> &text::BufferSnapshot {
303 &self.buffer_snapshot
304 }
305
306 #[ztracing::instrument(skip_all)]
307 pub fn hunks_intersecting_range<'a>(
308 &'a self,
309 range: Range<Anchor>,
310 buffer: &'a text::BufferSnapshot,
311 ) -> impl 'a + Iterator<Item = DiffHunk> {
312 let unstaged_counterpart = self.secondary_diff.as_deref();
313 let range = range.to_offset(buffer);
314 let filter = move |summary: &DiffHunkSummary| {
315 let summary_range = summary.buffer_range.to_offset(buffer);
316 let before_start = summary_range.end < range.start;
317 let after_end = summary_range.start > range.end;
318 !before_start && !after_end
319 };
320 self.hunks_intersecting_range_impl(filter, buffer, unstaged_counterpart)
321 }
322
323 /// Like [`hunks_intersecting_range`], but ignores optimistic pending hunks
324 /// (both secondary-status overrides and suppressions) and does not compute a
325 /// secondary status.
326 pub fn raw_hunks_intersecting_range<'a>(
327 &'a self,
328 range: Range<Anchor>,
329 buffer: &'a text::BufferSnapshot,
330 ) -> impl 'a + Iterator<Item = DiffHunk> {
331 let range = range.to_offset(buffer);
332 let filter = move |summary: &DiffHunkSummary| {
333 let summary_range = summary.buffer_range.to_offset(buffer);
334 !(summary_range.end < range.start) && !(summary_range.start > range.end)
335 };
336 self.hunks
337 .filter::<_, DiffHunkSummary>(buffer, filter)
338 .map(move |hunk| {
339 let buffer_range = hunk.buffer_range.clone();
340 DiffHunk {
341 range: buffer_range.to_point(buffer),
342 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
343 buffer_range,
344 secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
345 base_word_diffs: hunk.base_word_diffs.clone(),
346 buffer_word_diffs: hunk.buffer_word_diffs.clone(),
347 }
348 })
349 }
350
351 /// Maps a range in this diff's main buffer to the range it covers in the
352 /// base text, expanding to whole hunks wherever the range endpoints fall
353 /// inside or touch a hunk (`edit_for_old_position` is inclusive on both
354 /// boundaries, matching `raw_hunks_intersecting_range`). Used by the
355 /// index-write path to compute the index-coordinate footprint of a staging
356 /// operation; like the raw hunks, the mapping ignores optimistic pending
357 /// hunks.
358 pub fn base_text_range_for_buffer_range(
359 &self,
360 range: Range<Anchor>,
361 buffer: &text::BufferSnapshot,
362 ) -> Range<usize> {
363 let point_range = range.to_point(buffer);
364 let patch = self.patch_for_buffer_range(point_range.start..=point_range.end, buffer);
365 let start_point = patch.edit_for_old_position(point_range.start).new.start;
366 let end_point = patch.edit_for_old_position(point_range.end).new.end;
367 let base_text = self.base_text();
368 let start = base_text.point_to_offset(start_point.min(base_text.max_point()));
369 let end = base_text.point_to_offset(end_point.min(base_text.max_point()));
370 start.min(end)..end
371 }
372
373 pub fn hunks_intersecting_range_rev<'a>(
374 &'a self,
375 range: Range<Anchor>,
376 buffer: &'a text::BufferSnapshot,
377 ) -> impl 'a + Iterator<Item = DiffHunk> {
378 let filter = move |summary: &DiffHunkSummary| {
379 let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
380 let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
381 !before_start && !after_end
382 };
383 self.hunks_intersecting_range_rev_impl(filter, buffer)
384 }
385
386 pub fn hunks_intersecting_base_text_range<'a>(
387 &'a self,
388 range: Range<usize>,
389 main_buffer: &'a text::BufferSnapshot,
390 ) -> impl 'a + Iterator<Item = DiffHunk> {
391 let unstaged_counterpart = self.secondary_diff.as_deref();
392 let filter = move |summary: &DiffHunkSummary| {
393 let before_start = summary.diff_base_byte_range.end < range.start;
394 let after_end = summary.diff_base_byte_range.start > range.end;
395 !before_start && !after_end
396 };
397 self.hunks_intersecting_range_impl(filter, main_buffer, unstaged_counterpart)
398 }
399
400 pub fn hunks_intersecting_base_text_range_rev<'a>(
401 &'a self,
402 range: Range<usize>,
403 main_buffer: &'a text::BufferSnapshot,
404 ) -> impl 'a + Iterator<Item = DiffHunk> {
405 let filter = move |summary: &DiffHunkSummary| {
406 let before_start = summary.diff_base_byte_range.end.cmp(&range.start).is_lt();
407 let after_end = summary.diff_base_byte_range.start.cmp(&range.end).is_gt();
408 !before_start && !after_end
409 };
410 self.hunks_intersecting_range_rev_impl(filter, main_buffer)
411 }
412
413 pub fn hunks<'a>(
414 &'a self,
415 buffer_snapshot: &'a text::BufferSnapshot,
416 ) -> impl 'a + Iterator<Item = DiffHunk> {
417 self.hunks_intersecting_range(
418 Anchor::min_max_range_for_buffer(buffer_snapshot.remote_id()),
419 buffer_snapshot,
420 )
421 }
422
423 pub fn hunks_in_row_range<'a>(
424 &'a self,
425 range: Range<u32>,
426 buffer: &'a text::BufferSnapshot,
427 ) -> impl 'a + Iterator<Item = DiffHunk> {
428 let start = buffer.anchor_before(Point::new(range.start, 0));
429 let end = buffer.anchor_after(Point::new(range.end, 0));
430 self.hunks_intersecting_range(start..end, buffer)
431 }
432
433 pub fn range_to_hunk_range(
434 &self,
435 range: Range<Anchor>,
436 buffer: &text::BufferSnapshot,
437 ) -> (Option<Range<Anchor>>, Option<Range<usize>>) {
438 let first_hunk = self.hunks_intersecting_range(range.clone(), buffer).next();
439 let last_hunk = self.hunks_intersecting_range_rev(range, buffer).next();
440 let range = first_hunk
441 .as_ref()
442 .zip(last_hunk.as_ref())
443 .map(|(first, last)| first.buffer_range.start..last.buffer_range.end);
444 let base_text_range = first_hunk
445 .zip(last_hunk)
446 .map(|(first, last)| first.diff_base_byte_range.start..last.diff_base_byte_range.end);
447 (range, base_text_range)
448 }
449
450 pub fn base_text(&self) -> &language::BufferSnapshot {
451 &self.base_text
452 }
453
454 /// If this function returns `true`, the base texts are equal. If this
455 /// function returns `false`, they might be equal, but might not. This
456 /// result is used to avoid recalculating diffs in situations where we know
457 /// nothing has changed.
458 pub fn base_texts_definitely_eq(&self, other: &Self) -> bool {
459 if self.base_text_exists != other.base_text_exists {
460 return false;
461 }
462 let left = &self.base_text;
463 let right = &other.base_text;
464 let (old_id, old_version, old_empty) = (left.remote_id(), left.version(), left.is_empty());
465 let (new_id, new_version, new_empty) =
466 (right.remote_id(), right.version(), right.is_empty());
467 (new_id == old_id && new_version == old_version) || (new_empty && old_empty)
468 }
469
470 /// Returns the last hunk whose start is less than or equal to the given position.
471 fn hunk_before_base_text_offset<'a>(
472 &self,
473 target: usize,
474 cursor: &mut sum_tree::Cursor<'a, '_, InternalDiffHunk, DiffHunkSummary>,
475 ) -> Option<&'a InternalDiffHunk> {
476 cursor.seek_forward(&target, Bias::Left);
477 if cursor
478 .item()
479 .is_none_or(|hunk| target < hunk.diff_base_byte_range.start)
480 {
481 cursor.prev();
482 }
483 cursor
484 .item()
485 .filter(|hunk| target >= hunk.diff_base_byte_range.start)
486 }
487
488 fn hunk_before_buffer_anchor<'a>(
489 &self,
490 target: Anchor,
491 cursor: &mut sum_tree::Cursor<'a, '_, InternalDiffHunk, DiffHunkSummary>,
492 buffer: &text::BufferSnapshot,
493 ) -> Option<&'a InternalDiffHunk> {
494 cursor.seek_forward(&target, Bias::Left);
495 if cursor
496 .item()
497 .is_none_or(|hunk| target.cmp(&hunk.buffer_range.start, buffer).is_lt())
498 {
499 cursor.prev();
500 }
501 cursor
502 .item()
503 .filter(|hunk| target.cmp(&hunk.buffer_range.start, buffer).is_ge())
504 }
505
506 /// Returns a patch mapping the provided main buffer snapshot to the base text of this diff.
507 ///
508 /// The returned patch is guaranteed to be accurate for all main buffer points in the provided range,
509 /// but not necessarily for points outside that range.
510 pub fn patch_for_buffer_range<'a>(
511 &'a self,
512 range: RangeInclusive<Point>,
513 buffer: &'a text::BufferSnapshot,
514 ) -> Patch<Point> {
515 if !self.base_text_exists {
516 return Patch::new(vec![Edit {
517 old: Point::zero()..buffer.max_point(),
518 new: Point::zero()..Point::zero(),
519 }]);
520 }
521
522 let mut edits_since_diff = Patch::new(
523 buffer
524 .edits_since::<Point>(&self.buffer_snapshot.version)
525 .collect::<Vec<_>>(),
526 );
527 edits_since_diff.invert();
528
529 let mut start_point = edits_since_diff.old_to_new(*range.start());
530 if let Some(first_edit) = edits_since_diff.edits().first() {
531 start_point = start_point.min(first_edit.new.start);
532 }
533
534 let original_snapshot = self.original_buffer_snapshot();
535 let base_text = self.base_text();
536
537 let mut cursor = self.hunks.cursor(original_snapshot);
538 self.hunk_before_buffer_anchor(
539 original_snapshot.anchor_before(start_point),
540 &mut cursor,
541 original_snapshot,
542 );
543 if cursor.item().is_none() {
544 cursor.next();
545 }
546
547 let mut prefix_edit = cursor.prev_item().map(|prev_hunk| Edit {
548 old: Point::zero()..prev_hunk.buffer_range.end.to_point(original_snapshot),
549 new: Point::zero()..prev_hunk.diff_base_byte_range.end.to_point(base_text),
550 });
551
552 let mut range_end = edits_since_diff.old_to_new(*range.end());
553 if let Some(last_edit) = edits_since_diff.edits().last() {
554 range_end = range_end.max(last_edit.new.end);
555 }
556 let range_end = original_snapshot.anchor_before(range_end);
557
558 let hunk_iter = std::iter::from_fn(move || {
559 if let Some(edit) = prefix_edit.take() {
560 return Some(edit);
561 }
562 let hunk = cursor.item()?;
563 if hunk
564 .buffer_range
565 .start
566 .cmp(&range_end, original_snapshot)
567 .is_gt()
568 {
569 return None;
570 }
571 let edit = Edit {
572 old: hunk.buffer_range.to_point(original_snapshot),
573 new: hunk.diff_base_byte_range.to_point(base_text),
574 };
575 cursor.next();
576 Some(edit)
577 });
578
579 edits_since_diff.compose(hunk_iter)
580 }
581
582 #[cfg(test)]
583 pub(crate) fn patch_for_buffer_range_naive<'a>(
584 &'a self,
585 buffer: &'a text::BufferSnapshot,
586 ) -> Patch<Point> {
587 let original_snapshot = self.original_buffer_snapshot();
588
589 let edits_since: Vec<Edit<Point>> = buffer
590 .edits_since::<Point>(original_snapshot.version())
591 .collect();
592 let mut inverted_edits_since = Patch::new(edits_since);
593 inverted_edits_since.invert();
594
595 inverted_edits_since.compose(
596 self.hunks
597 .iter()
598 .map(|hunk| {
599 let old_start = hunk.buffer_range.start.to_point(original_snapshot);
600 let old_end = hunk.buffer_range.end.to_point(original_snapshot);
601 let new_start = self
602 .base_text()
603 .offset_to_point(hunk.diff_base_byte_range.start);
604 let new_end = self
605 .base_text()
606 .offset_to_point(hunk.diff_base_byte_range.end);
607 Edit {
608 old: old_start..old_end,
609 new: new_start..new_end,
610 }
611 })
612 .chain(if !self.base_text_exists && self.hunks.is_empty() {
613 Some(Edit {
614 old: Point::zero()..original_snapshot.max_point(),
615 new: Point::zero()..Point::zero(),
616 })
617 } else {
618 None
619 }),
620 )
621 }
622
623 /// Returns a patch mapping the base text of this diff to the provided main buffer snapshot.
624 ///
625 /// The returned patch is guaranteed to be accurate for all base text points in the provided range,
626 /// but not necessarily for points outside that range.
627 pub fn patch_for_base_text_range<'a>(
628 &'a self,
629 range: RangeInclusive<Point>,
630 buffer: &'a text::BufferSnapshot,
631 ) -> Patch<Point> {
632 if !self.base_text_exists {
633 return Patch::new(vec![Edit {
634 old: Point::zero()..Point::zero(),
635 new: Point::zero()..buffer.max_point(),
636 }]);
637 }
638
639 let edits_since_diff = buffer
640 .edits_since::<Point>(&self.buffer_snapshot.version)
641 .collect::<Vec<_>>();
642
643 let mut hunk_patch = Vec::new();
644 let mut cursor = self.hunks.cursor(self.original_buffer_snapshot());
645 let hunk_before = self
646 .hunk_before_base_text_offset(range.start().to_offset(self.base_text()), &mut cursor);
647
648 if let Some(hunk) = hunk_before
649 && let Some(first_edit) = edits_since_diff.first()
650 && hunk
651 .buffer_range
652 .start
653 .to_point(self.original_buffer_snapshot())
654 > first_edit.old.start
655 {
656 cursor.reset();
657 self.hunk_before_buffer_anchor(
658 self.original_buffer_snapshot()
659 .anchor_before(first_edit.old.start),
660 &mut cursor,
661 self.original_buffer_snapshot(),
662 );
663 }
664 if cursor.item().is_none() {
665 cursor.next();
666 }
667 if let Some(prev_hunk) = cursor.prev_item() {
668 hunk_patch.push(Edit {
669 old: Point::zero()
670 ..prev_hunk
671 .diff_base_byte_range
672 .end
673 .to_point(self.base_text()),
674 new: Point::zero()
675 ..prev_hunk
676 .buffer_range
677 .end
678 .to_point(self.original_buffer_snapshot()),
679 })
680 }
681 let range_end = range.end().to_offset(self.base_text());
682 while let Some(hunk) = cursor.item()
683 && (hunk.diff_base_byte_range.start <= range_end
684 || edits_since_diff.last().is_some_and(|last_edit| {
685 hunk.buffer_range
686 .start
687 .to_point(self.original_buffer_snapshot())
688 <= last_edit.old.end
689 }))
690 {
691 hunk_patch.push(Edit {
692 old: hunk.diff_base_byte_range.to_point(self.base_text()),
693 new: hunk.buffer_range.to_point(self.original_buffer_snapshot()),
694 });
695 cursor.next();
696 }
697
698 Patch::new(hunk_patch).compose(edits_since_diff)
699 }
700
701 #[cfg(test)]
702 pub(crate) fn patch_for_base_text_range_naive<'a>(
703 &'a self,
704 buffer: &'a text::BufferSnapshot,
705 ) -> Patch<Point> {
706 let original_snapshot = self.original_buffer_snapshot();
707
708 let mut hunk_edits: Vec<Edit<Point>> = Vec::new();
709 for hunk in self.hunks.iter() {
710 let old_start = self
711 .base_text()
712 .offset_to_point(hunk.diff_base_byte_range.start);
713 let old_end = self
714 .base_text()
715 .offset_to_point(hunk.diff_base_byte_range.end);
716 let new_start = hunk.buffer_range.start.to_point(original_snapshot);
717 let new_end = hunk.buffer_range.end.to_point(original_snapshot);
718 hunk_edits.push(Edit {
719 old: old_start..old_end,
720 new: new_start..new_end,
721 });
722 }
723 if !self.base_text_exists && hunk_edits.is_empty() {
724 hunk_edits.push(Edit {
725 old: Point::zero()..Point::zero(),
726 new: Point::zero()..original_snapshot.max_point(),
727 })
728 }
729 let hunk_patch = Patch::new(hunk_edits);
730
731 hunk_patch.compose(buffer.edits_since::<Point>(original_snapshot.version()))
732 }
733
734 pub fn buffer_point_to_base_text_range(
735 &self,
736 point: Point,
737 buffer: &text::BufferSnapshot,
738 ) -> Range<Point> {
739 let patch = self.patch_for_buffer_range(point..=point, buffer);
740 let edit = patch.edit_for_old_position(point);
741 edit.new
742 }
743
744 pub fn base_text_point_to_buffer_range(
745 &self,
746 point: Point,
747 buffer: &text::BufferSnapshot,
748 ) -> Range<Point> {
749 let patch = self.patch_for_base_text_range(point..=point, buffer);
750 let edit = patch.edit_for_old_position(point);
751 edit.new
752 }
753
754 pub fn buffer_point_to_base_text_point(
755 &self,
756 point: Point,
757 buffer: &text::BufferSnapshot,
758 ) -> Point {
759 let patch = self.patch_for_buffer_range(point..=point, buffer);
760 let edit = patch.edit_for_old_position(point);
761 if point == edit.old.end {
762 edit.new.end
763 } else {
764 edit.new.start
765 }
766 }
767
768 pub fn base_text_point_to_buffer_point(
769 &self,
770 point: Point,
771 buffer: &text::BufferSnapshot,
772 ) -> Point {
773 let patch = self.patch_for_base_text_range(point..=point, buffer);
774 let edit = patch.edit_for_old_position(point);
775 if point == edit.old.end {
776 edit.new.end
777 } else {
778 edit.new.start
779 }
780 }
781}
782
783impl BufferDiffSnapshot {
784 // Compute the edits to apply to the index, and the resulting pending hunks,
785 // for a stage or unstage operation on the uncommitted diff.
786 pub fn compute_uncommitted_index_edits(
787 &self,
788 unstaged_diff: &Self,
789 stage: bool,
790 hunks: &[DiffHunk],
791 buffer: &text::BufferSnapshot,
792 file_exists: bool,
793 ) -> (Option<Vec<(Range<usize>, Arc<str>)>>, Vec<PendingHunk>) {
794 let head_text = self
795 .base_text_exists
796 .then(|| self.base_text.as_rope().clone());
797 let index_text = unstaged_diff
798 .base_text_exists
799 .then(|| unstaged_diff.base_text.as_rope().clone());
800 let sense = PendingSense::SetSecondaryStatus { stage };
801 let version = buffer.version().clone();
802
803 // If the file doesn't exist in either HEAD or the index, then the
804 // entire file must be either created or deleted in the index.
805 let (index_text, head_text) = match (index_text, head_text) {
806 (Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
807 (index_text, head_text) => {
808 let index_len = index_text.as_ref().map_or(0, |rope| rope.len());
809 let new_index_text: Option<Rope> = if stage {
810 log::debug!("stage all");
811 file_exists.then(|| buffer.as_rope().clone())
812 } else {
813 log::debug!("unstage all");
814 head_text
815 };
816
817 let pending = vec![PendingHunk::new(
818 Anchor::min_max_range_for_buffer(buffer.remote_id()),
819 0..index_len,
820 version,
821 sense,
822 )];
823 let edits =
824 new_index_text.map(|rope| vec![(0..index_len, Arc::from(rope.to_string()))]);
825 return (edits, pending);
826 }
827 };
828
829 let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
830 unstaged_hunk_cursor.next();
831
832 let mut prev_unstaged_hunk_buffer_end = 0;
833 let mut prev_unstaged_hunk_base_text_end = 0;
834 let mut edits = Vec::<(Range<usize>, Arc<str>)>::new();
835 let mut pending = Vec::<PendingHunk>::new();
836
837 // Process only the hunks the user acted on, skipping any already in the
838 // desired state.
839 let mut hunks_iter = hunks
840 .iter()
841 .filter(|hunk| {
842 !((stage && hunk.secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
843 || (!stage
844 && hunk.secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk))
845 })
846 .peekable();
847
848 while let Some(hunk) = hunks_iter.next() {
849 let buffer_range = hunk.buffer_range.clone();
850 let diff_base_byte_range = hunk.diff_base_byte_range.clone();
851 pending.push(PendingHunk::new(
852 buffer_range.clone(),
853 diff_base_byte_range.clone(),
854 version.clone(),
855 sense,
856 ));
857
858 // Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk
859 let skipped_unstaged = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left);
860
861 if let Some(unstaged_hunk) = skipped_unstaged.last() {
862 prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
863 prev_unstaged_hunk_buffer_end = unstaged_hunk.buffer_range.end.to_offset(buffer);
864 }
865
866 // Find where this hunk is in the index if it doesn't overlap
867 let mut buffer_offset_range = buffer_range.to_offset(buffer);
868 let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_end;
869 let mut index_start = prev_unstaged_hunk_base_text_end + start_overshoot;
870
871 loop {
872 // Merge this hunk with any overlapping unstaged hunks.
873 if let Some(unstaged_hunk) = unstaged_hunk_cursor.item() {
874 let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer);
875 if unstaged_hunk_offset_range.start <= buffer_offset_range.end {
876 prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
877 prev_unstaged_hunk_buffer_end = unstaged_hunk_offset_range.end;
878
879 index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start);
880 buffer_offset_range.start = buffer_offset_range
881 .start
882 .min(unstaged_hunk_offset_range.start);
883 buffer_offset_range.end =
884 buffer_offset_range.end.max(unstaged_hunk_offset_range.end);
885
886 unstaged_hunk_cursor.next();
887 continue;
888 }
889 }
890
891 // If any unstaged hunks were merged, then subsequent acted-on hunks
892 // may now overlap this hunk. Merge them.
893 if let Some(next_hunk) = hunks_iter.peek() {
894 let next_hunk_offset_range = next_hunk.buffer_range.to_offset(buffer);
895 if next_hunk_offset_range.start <= buffer_offset_range.end {
896 buffer_offset_range.end =
897 buffer_offset_range.end.max(next_hunk_offset_range.end);
898 let merged_hunk = hunks_iter.next().expect("peeked hunk exists");
899 pending.push(PendingHunk::new(
900 merged_hunk.buffer_range.clone(),
901 merged_hunk.diff_base_byte_range.clone(),
902 version.clone(),
903 sense,
904 ));
905 continue;
906 }
907 }
908
909 break;
910 }
911
912 let end_overshoot = buffer_offset_range
913 .end
914 .saturating_sub(prev_unstaged_hunk_buffer_end);
915 let index_end = prev_unstaged_hunk_base_text_end + end_overshoot;
916
917 // Clamp to the index text bounds. The overshoot mapping assumes that
918 // text between unstaged hunks is identical in the buffer and index.
919 // When the buffer has been edited since the diff was computed, anchor
920 // positions shift while diff_base_byte_range values don't, which can
921 // cause index_end to exceed index_text.len().
922 // See `test_stage_all_with_stale_buffer` which would hit an assert
923 // without these min calls
924 let index_end = index_end.min(index_text.len());
925 let index_start = index_start.min(index_end);
926 let index_byte_range = index_start..index_end;
927
928 let replacement_text: Arc<str> = if stage {
929 log::debug!("staging hunk {:?}", buffer_offset_range);
930 Arc::from(
931 buffer
932 .text_for_range(buffer_offset_range)
933 .collect::<String>(),
934 )
935 } else {
936 log::debug!("unstaging hunk {:?}", buffer_offset_range);
937 Arc::from(
938 head_text
939 .chunks_in_range(diff_base_byte_range.clone())
940 .collect::<String>(),
941 )
942 };
943
944 // Distinct worktree hunks can project to touching index ranges
945 // (e.g. a staged deletion ending exactly where the next hunk's
946 // index position starts). Merge them so the edit list stays
947 // strictly disjoint, which the pending-edit eviction logic relies
948 // on to not evict one of these edits when the other is inserted.
949 if let Some((last_range, last_text)) = edits.last_mut()
950 && index_byte_range.start <= last_range.end
951 {
952 debug_assert!(index_byte_range.start == last_range.end);
953 debug_assert!(index_byte_range.end >= last_range.end);
954 last_range.end = index_byte_range.end;
955 let mut merged_text =
956 String::with_capacity(last_text.len() + replacement_text.len());
957 merged_text.push_str(last_text);
958 merged_text.push_str(&replacement_text);
959 *last_text = Arc::from(merged_text);
960 } else {
961 edits.push((index_byte_range, replacement_text));
962 }
963 }
964
965 #[cfg(debug_assertions)] // invariants: non-overlapping and sorted
966 {
967 for window in edits.windows(2) {
968 let (range_a, range_b) = (&window[0].0, &window[1].0);
969 debug_assert!(
970 range_a.end < range_b.start,
971 "index edits out of order or overlapping: {:?}",
972 edits
973 .iter()
974 .map(|(range, text)| (range.clone(), text.len()))
975 .collect::<Vec<_>>()
976 );
977 }
978 }
979
980 (Some(edits), pending)
981 }
982}
983
984impl BufferDiffSnapshot {
985 fn hunks_intersecting_range_impl<'a>(
986 &'a self,
987 filter: impl 'a + Fn(&DiffHunkSummary) -> bool,
988 buffer: &'a text::BufferSnapshot,
989 secondary: Option<&'a Self>,
990 ) -> impl 'a + Iterator<Item = DiffHunk> {
991 let anchor_iter = self
992 .hunks
993 .filter::<_, DiffHunkSummary>(buffer, filter)
994 .flat_map(move |hunk| {
995 [
996 (
997 hunk.buffer_range.start,
998 (
999 hunk.buffer_range.start,
1000 hunk.diff_base_byte_range.start,
1001 hunk,
1002 ),
1003 ),
1004 (
1005 hunk.buffer_range.end,
1006 (hunk.buffer_range.end, hunk.diff_base_byte_range.end, hunk),
1007 ),
1008 ]
1009 });
1010
1011 let mut pending_hunks_cursor = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
1012 pending_hunks_cursor.next();
1013
1014 let mut secondary_cursor = None;
1015 if let Some(secondary) = secondary.as_ref() {
1016 let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
1017 cursor.next();
1018 secondary_cursor = Some(cursor);
1019 }
1020
1021 let max_point = buffer.max_point();
1022 let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
1023 iter::from_fn(move || {
1024 loop {
1025 let (start_point, (start_anchor, start_base, hunk)) = summaries.next()?;
1026 let (mut end_point, (mut end_anchor, end_base, _)) = summaries.next()?;
1027
1028 let base_word_diffs = hunk.base_word_diffs.clone();
1029 let buffer_word_diffs = hunk.buffer_word_diffs.clone();
1030
1031 if !start_anchor.is_valid(buffer) {
1032 continue;
1033 }
1034
1035 if end_point.column > 0 && end_point < max_point {
1036 end_point.row += 1;
1037 end_point.column = 0;
1038 end_anchor = buffer.anchor_before(end_point);
1039 }
1040
1041 let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
1042
1043 let mut has_pending = false;
1044 if start_anchor
1045 .cmp(&pending_hunks_cursor.start().buffer_range.start, buffer)
1046 .is_gt()
1047 {
1048 pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left);
1049 }
1050
1051 if let Some(pending_hunk) = pending_hunks_cursor.item() {
1052 let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
1053 if pending_range.end.column > 0 {
1054 pending_range.end.row += 1;
1055 pending_range.end.column = 0;
1056 }
1057
1058 if pending_range == (start_point..end_point)
1059 && !buffer.has_edits_since_in_range(
1060 &pending_hunk.buffer_version,
1061 start_anchor..end_anchor,
1062 )
1063 {
1064 match pending_hunk.sense {
1065 PendingSense::SetSecondaryStatus { stage } => {
1066 has_pending = true;
1067 secondary_status = if stage {
1068 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
1069 } else {
1070 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
1071 };
1072 }
1073 PendingSense::Suppress => continue,
1074 }
1075 }
1076 }
1077
1078 if let (Some(secondary_cursor), false) = (secondary_cursor.as_mut(), has_pending) {
1079 if start_anchor
1080 .cmp(&secondary_cursor.start().buffer_range.start, buffer)
1081 .is_gt()
1082 {
1083 secondary_cursor.seek_forward(&start_anchor, Bias::Left);
1084 }
1085
1086 if let Some(secondary_hunk) = secondary_cursor.item() {
1087 let mut secondary_range = secondary_hunk.buffer_range.to_point(buffer);
1088 if secondary_range.end.column > 0 {
1089 secondary_range.end.row += 1;
1090 secondary_range.end.column = 0;
1091 }
1092 if secondary_range.is_empty()
1093 && secondary_hunk.diff_base_byte_range.is_empty()
1094 {
1095 // ignore
1096 } else if secondary_range == (start_point..end_point) {
1097 secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
1098 } else if secondary_range.start <= end_point {
1099 secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
1100 }
1101 }
1102 }
1103
1104 return Some(DiffHunk {
1105 range: start_point..end_point,
1106 diff_base_byte_range: start_base..end_base,
1107 buffer_range: start_anchor..end_anchor,
1108 base_word_diffs,
1109 buffer_word_diffs,
1110 secondary_status,
1111 });
1112 }
1113 })
1114 }
1115
1116 fn hunks_intersecting_range_rev_impl<'a>(
1117 &'a self,
1118 filter: impl 'a + Fn(&DiffHunkSummary) -> bool,
1119 buffer: &'a text::BufferSnapshot,
1120 ) -> impl 'a + Iterator<Item = DiffHunk> {
1121 let mut cursor = self.hunks.filter::<_, DiffHunkSummary>(buffer, filter);
1122
1123 iter::from_fn(move || {
1124 cursor.prev();
1125
1126 let hunk = cursor.item()?;
1127 let range = hunk.buffer_range.to_point(buffer);
1128
1129 Some(DiffHunk {
1130 range,
1131 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
1132 buffer_range: hunk.buffer_range.clone(),
1133 // The secondary status is not used by callers of this method.
1134 secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
1135 base_word_diffs: hunk.base_word_diffs.clone(),
1136 buffer_word_diffs: hunk.buffer_word_diffs.clone(),
1137 })
1138 })
1139 }
1140}
1141
1142fn build_diff_options(
1143 language: Option<LanguageName>,
1144 language_scope: Option<language::LanguageScope>,
1145 cx: &App,
1146) -> Option<DiffOptions> {
1147 #[cfg(any(test, feature = "test-support"))]
1148 {
1149 if !cx.has_global::<settings::SettingsStore>() {
1150 return Some(DiffOptions {
1151 language_scope,
1152 max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
1153 ..Default::default()
1154 });
1155 }
1156 }
1157
1158 LanguageSettings::resolve(None, language.as_ref(), cx)
1159 .word_diff_enabled
1160 .then_some(DiffOptions {
1161 language_scope,
1162 max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
1163 ..Default::default()
1164 })
1165}
1166
1167fn compute_hunks(
1168 diff_base: Option<(Arc<str>, Rope)>,
1169 buffer: &text::BufferSnapshot,
1170 diff_options: Option<DiffOptions>,
1171) -> SumTree<InternalDiffHunk> {
1172 let mut tree = SumTree::new(buffer);
1173
1174 if let Some((diff_base, diff_base_rope)) = diff_base {
1175 let buffer_text = buffer.as_rope().to_string();
1176
1177 // A common case in Zed is that the empty buffer is represented as just a newline,
1178 // but if we just compute a naive diff you get a "preserved" line in the middle,
1179 // which is a bit odd.
1180 if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
1181 tree.push(
1182 InternalDiffHunk {
1183 buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
1184 diff_base_byte_range: 0..diff_base.len() - 1,
1185 diff_base_point_range: Point::new(0, 0)
1186 ..diff_base_rope.offset_to_point(diff_base.len() - 1),
1187 base_word_diffs: Vec::default(),
1188 buffer_word_diffs: Vec::default(),
1189 },
1190 buffer,
1191 );
1192 return tree;
1193 }
1194
1195 let input = InternedInput::new(lines(diff_base.as_ref()), lines(buffer_text.as_str()));
1196 let mut diff = Diff::compute(Algorithm::Histogram, &input);
1197 // Canonicalize the placement of ambiguous hunks (git's slider/indent
1198 // heuristic). Without this, diffs of the same buffer against different
1199 // base texts (e.g. HEAD vs index) can anchor the same logical change at
1200 // different rows, and code that correlates hunks across those diffs
1201 // misbehaves: hunks render as staged when they aren't, and staging or
1202 // unstaging them corrupts the index.
1203 diff.postprocess_lines(&input);
1204 let mut sink = HunkSink::new(&diff_base, &diff_base_rope, buffer, diff_options.as_ref());
1205 for hunk in diff.hunks() {
1206 sink.process_change(hunk.before, hunk.after);
1207 }
1208 for hunk in sink.finish() {
1209 tree.push(hunk, buffer);
1210 }
1211 } else {
1212 tree.push(
1213 InternalDiffHunk {
1214 buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
1215 diff_base_byte_range: 0..0,
1216 diff_base_point_range: Point::new(0, 0)..Point::new(0, 0),
1217 base_word_diffs: Vec::default(),
1218 buffer_word_diffs: Vec::default(),
1219 },
1220 buffer,
1221 );
1222 }
1223
1224 tree
1225}
1226struct HunkSink<'a> {
1227 diff_base_rope: &'a Rope,
1228 buffer: &'a text::BufferSnapshot,
1229 diff_options: Option<&'a DiffOptions>,
1230 old_line_offsets: Vec<usize>,
1231 hunks: Vec<InternalDiffHunk>,
1232}
1233
1234impl<'a> HunkSink<'a> {
1235 fn new(
1236 diff_base: &'a str,
1237 diff_base_rope: &'a Rope,
1238 buffer: &'a text::BufferSnapshot,
1239 diff_options: Option<&'a DiffOptions>,
1240 ) -> Self {
1241 let old_line_offsets = Self::compute_line_offsets(diff_base);
1242 Self {
1243 diff_base_rope,
1244 buffer,
1245 diff_options,
1246 old_line_offsets,
1247 hunks: Vec::new(),
1248 }
1249 }
1250
1251 fn compute_line_offsets(text: &str) -> Vec<usize> {
1252 let mut offsets = vec![0];
1253 let mut offset = 0;
1254 for line in lines(text) {
1255 offset += line.len();
1256 offsets.push(offset);
1257 }
1258 offsets
1259 }
1260}
1261
1262impl HunkSink<'_> {
1263 fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
1264 let old_start = before.start as usize;
1265 let old_end = before.end as usize;
1266 let new_start = after.start as usize;
1267 let new_end = after.end as usize;
1268
1269 let diff_base_byte_range = self.old_line_offsets[old_start]..self.old_line_offsets[old_end];
1270
1271 let buffer_row_range = (new_start as u32)..(new_end as u32);
1272
1273 let start = Point::new(buffer_row_range.start, 0);
1274 let end = Point::new(buffer_row_range.end, 0);
1275 let buffer_range = self.buffer.anchor_before(start)..self.buffer.anchor_before(end);
1276
1277 let base_line_count = old_end - old_start;
1278 let buffer_line_count = new_end - new_start;
1279
1280 let (base_word_diffs, buffer_word_diffs) = if let Some(diff_options) = self.diff_options
1281 && !buffer_row_range.is_empty()
1282 && base_line_count == buffer_line_count
1283 && diff_options.max_word_diff_line_count >= base_line_count
1284 {
1285 let base_text: String = self
1286 .diff_base_rope
1287 .chunks_in_range(diff_base_byte_range.clone())
1288 .collect();
1289 let buffer_text: String = self.buffer.text_for_range(buffer_range.clone()).collect();
1290
1291 let (base_word_diffs, buffer_word_diffs_relative) = word_diff_ranges(
1292 &base_text,
1293 &buffer_text,
1294 DiffOptions {
1295 language_scope: diff_options.language_scope.clone(),
1296 ..*diff_options
1297 },
1298 );
1299
1300 let buffer_start_offset = buffer_range.start.to_offset(self.buffer);
1301 let buffer_word_diffs = buffer_word_diffs_relative
1302 .into_iter()
1303 .map(|range| {
1304 let start = self.buffer.anchor_after(buffer_start_offset + range.start);
1305 let end = self.buffer.anchor_after(buffer_start_offset + range.end);
1306 start..end
1307 })
1308 .collect();
1309
1310 (base_word_diffs, buffer_word_diffs)
1311 } else {
1312 (Vec::default(), Vec::default())
1313 };
1314
1315 self.hunks.push(InternalDiffHunk {
1316 buffer_range,
1317 diff_base_byte_range: diff_base_byte_range.clone(),
1318 diff_base_point_range: self
1319 .diff_base_rope
1320 .offset_to_point(diff_base_byte_range.start)
1321 ..self
1322 .diff_base_rope
1323 .offset_to_point(diff_base_byte_range.end),
1324 base_word_diffs,
1325 buffer_word_diffs,
1326 });
1327 }
1328
1329 fn finish(self) -> Vec<InternalDiffHunk> {
1330 self.hunks
1331 }
1332}
1333
1334fn compare_hunks(
1335 new_hunks: &SumTree<InternalDiffHunk>,
1336 old_hunks: &SumTree<InternalDiffHunk>,
1337 old_snapshot: &text::BufferSnapshot,
1338 new_snapshot: &text::BufferSnapshot,
1339 old_base_text: &text::BufferSnapshot,
1340 new_base_text: &text::BufferSnapshot,
1341) -> DiffChanged {
1342 let mut new_cursor = new_hunks.cursor::<()>(new_snapshot);
1343 let mut old_cursor = old_hunks.cursor::<()>(new_snapshot);
1344 old_cursor.next();
1345 new_cursor.next();
1346 let mut start = None;
1347 let mut end = None;
1348 let mut base_text_start: Option<Anchor> = None;
1349 let mut base_text_end: Option<Anchor> = None;
1350
1351 let mut last_unchanged_new_hunk_end: Option<text::Anchor> = None;
1352 let mut has_changes = false;
1353 let mut extended_end_candidate: Option<text::Anchor> = None;
1354
1355 loop {
1356 match (new_cursor.item(), old_cursor.item()) {
1357 (Some(new_hunk), Some(old_hunk)) => {
1358 match new_hunk
1359 .buffer_range
1360 .start
1361 .cmp(&old_hunk.buffer_range.start, new_snapshot)
1362 {
1363 Ordering::Less => {
1364 has_changes = true;
1365 extended_end_candidate = None;
1366 start.get_or_insert(new_hunk.buffer_range.start);
1367 base_text_start.get_or_insert(
1368 new_base_text.anchor_before(new_hunk.diff_base_byte_range.start),
1369 );
1370 end.replace(new_hunk.buffer_range.end);
1371 let new_diff_range_end =
1372 new_base_text.anchor_after(new_hunk.diff_base_byte_range.end);
1373 if base_text_end.is_none_or(|base_text_end| {
1374 new_diff_range_end
1375 .cmp(&base_text_end, &new_base_text)
1376 .is_gt()
1377 }) {
1378 base_text_end = Some(new_diff_range_end)
1379 }
1380 new_cursor.next();
1381 }
1382 Ordering::Equal => {
1383 if new_hunk != old_hunk {
1384 has_changes = true;
1385 extended_end_candidate = None;
1386 start.get_or_insert(new_hunk.buffer_range.start);
1387 base_text_start.get_or_insert(
1388 new_base_text.anchor_before(new_hunk.diff_base_byte_range.start),
1389 );
1390 if old_hunk
1391 .buffer_range
1392 .end
1393 .cmp(&new_hunk.buffer_range.end, new_snapshot)
1394 .is_ge()
1395 {
1396 end.replace(old_hunk.buffer_range.end);
1397 } else {
1398 end.replace(new_hunk.buffer_range.end);
1399 }
1400
1401 let old_hunk_diff_base_range_end =
1402 old_base_text.anchor_after(old_hunk.diff_base_byte_range.end);
1403 let new_hunk_diff_base_range_end =
1404 new_base_text.anchor_after(new_hunk.diff_base_byte_range.end);
1405
1406 base_text_end.replace(
1407 *old_hunk_diff_base_range_end
1408 .max(&new_hunk_diff_base_range_end, new_base_text),
1409 );
1410 } else {
1411 if !has_changes {
1412 last_unchanged_new_hunk_end = Some(new_hunk.buffer_range.end);
1413 } else if extended_end_candidate.is_none() {
1414 extended_end_candidate = Some(new_hunk.buffer_range.start);
1415 }
1416 }
1417
1418 new_cursor.next();
1419 old_cursor.next();
1420 }
1421 Ordering::Greater => {
1422 has_changes = true;
1423 extended_end_candidate = None;
1424 start.get_or_insert(old_hunk.buffer_range.start);
1425 base_text_start.get_or_insert(
1426 old_base_text.anchor_after(old_hunk.diff_base_byte_range.start),
1427 );
1428 end.replace(old_hunk.buffer_range.end);
1429 let old_diff_range_end =
1430 old_base_text.anchor_after(old_hunk.diff_base_byte_range.end);
1431 if base_text_end.is_none_or(|base_text_end| {
1432 old_diff_range_end
1433 .cmp(&base_text_end, new_base_text)
1434 .is_gt()
1435 }) {
1436 base_text_end = Some(old_diff_range_end)
1437 }
1438 old_cursor.next();
1439 }
1440 }
1441 }
1442 (Some(new_hunk), None) => {
1443 has_changes = true;
1444 extended_end_candidate = None;
1445 start.get_or_insert(new_hunk.buffer_range.start);
1446 base_text_start
1447 .get_or_insert(new_base_text.anchor_after(new_hunk.diff_base_byte_range.start));
1448 if end.is_none_or(|end| end.cmp(&new_hunk.buffer_range.end, &new_snapshot).is_le())
1449 {
1450 end.replace(new_hunk.buffer_range.end);
1451 }
1452 let new_base_text_end =
1453 new_base_text.anchor_after(new_hunk.diff_base_byte_range.end);
1454 if base_text_end.is_none_or(|base_text_end| {
1455 new_base_text_end.cmp(&base_text_end, new_base_text).is_gt()
1456 }) {
1457 base_text_end = Some(new_base_text_end)
1458 }
1459 new_cursor.next();
1460 }
1461 (None, Some(old_hunk)) => {
1462 has_changes = true;
1463 extended_end_candidate = None;
1464 start.get_or_insert(old_hunk.buffer_range.start);
1465 base_text_start
1466 .get_or_insert(old_base_text.anchor_after(old_hunk.diff_base_byte_range.start));
1467 if end.is_none_or(|end| end.cmp(&old_hunk.buffer_range.end, &new_snapshot).is_le())
1468 {
1469 end.replace(old_hunk.buffer_range.end);
1470 }
1471 let old_base_text_end =
1472 old_base_text.anchor_after(old_hunk.diff_base_byte_range.end);
1473 if base_text_end.is_none_or(|base_text_end| {
1474 old_base_text_end.cmp(&base_text_end, new_base_text).is_gt()
1475 }) {
1476 base_text_end = Some(old_base_text_end);
1477 }
1478 old_cursor.next();
1479 }
1480 (None, None) => break,
1481 }
1482 }
1483
1484 let changed_range = start.zip(end).map(|(start, end)| start..end);
1485 let base_text_changed_range = base_text_start
1486 .zip(base_text_end)
1487 .map(|(start, end)| (start..end).to_offset(new_base_text));
1488
1489 let extended_range = if has_changes && let Some(changed_range) = changed_range.clone() {
1490 let extended_start = *last_unchanged_new_hunk_end
1491 .unwrap_or(text::Anchor::min_for_buffer(new_snapshot.remote_id()))
1492 .min(&changed_range.start, new_snapshot);
1493 let extended_start = new_snapshot
1494 .anchored_edits_since_in_range::<usize>(
1495 &old_snapshot.version(),
1496 extended_start..changed_range.start,
1497 )
1498 .map(|(_, anchors)| anchors.start)
1499 .min_by(|a, b| a.cmp(b, new_snapshot))
1500 .unwrap_or(changed_range.start);
1501
1502 let extended_end = *extended_end_candidate
1503 .unwrap_or(text::Anchor::max_for_buffer(new_snapshot.remote_id()))
1504 .max(&changed_range.end, new_snapshot);
1505 let extended_end = new_snapshot
1506 .anchored_edits_since_in_range::<usize>(
1507 &old_snapshot.version(),
1508 changed_range.end..extended_end,
1509 )
1510 .map(|(_, anchors)| anchors.end)
1511 .max_by(|a, b| a.cmp(b, new_snapshot))
1512 .unwrap_or(changed_range.end);
1513
1514 Some(extended_start..extended_end)
1515 } else {
1516 None
1517 };
1518
1519 DiffChanged {
1520 changed_range,
1521 base_text_changed_range,
1522 extended_range,
1523 base_text_changed: false,
1524 }
1525}
1526
1527impl std::fmt::Debug for BufferDiff {
1528 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1529 f.debug_struct("BufferChangeSet")
1530 .field("buffer_id", &self.buffer_id)
1531 .finish()
1532 }
1533}
1534
1535#[derive(Clone, Debug, Default)]
1536pub struct DiffChanged {
1537 pub changed_range: Option<Range<text::Anchor>>,
1538 pub base_text_changed_range: Option<Range<usize>>,
1539 pub extended_range: Option<Range<text::Anchor>>,
1540 pub base_text_changed: bool,
1541}
1542
1543#[derive(Clone, Debug)]
1544pub enum BufferDiffEvent {
1545 BaseTextChanged,
1546 DiffChanged(DiffChanged),
1547}
1548
1549impl EventEmitter<BufferDiffEvent> for BufferDiff {}
1550
1551impl BufferDiff {
1552 pub fn new(
1553 buffer: &text::BufferSnapshot,
1554 language: Option<Arc<Language>>,
1555 language_registry: Option<Arc<LanguageRegistry>>,
1556 cx: &mut App,
1557 ) -> Self {
1558 let base_text = cx.new(|cx| {
1559 let mut base_buffer = language::Buffer::local("", cx);
1560 base_buffer.set_capability(Capability::ReadOnly, cx);
1561 if let Some(language_registry) = language_registry {
1562 base_buffer.set_language_registry(language_registry);
1563 }
1564 base_buffer.set_language_async(language, cx);
1565 base_buffer
1566 });
1567
1568 BufferDiff {
1569 buffer_id: buffer.remote_id(),
1570 base_text_buffer: base_text,
1571 diff_snapshot: None,
1572 buffer_snapshot: buffer.clone(),
1573 secondary_diff: None,
1574 }
1575 }
1576
1577 pub fn new_with_base_text_buffer(
1578 buffer: &text::BufferSnapshot,
1579 base_text_buffer: Entity<language::Buffer>,
1580 _cx: &mut App,
1581 ) -> Self {
1582 BufferDiff {
1583 buffer_id: buffer.remote_id(),
1584 base_text_buffer,
1585 diff_snapshot: None,
1586 buffer_snapshot: buffer.clone(),
1587 secondary_diff: None,
1588 }
1589 }
1590
1591 pub fn new_unchanged(
1592 buffer: &text::BufferSnapshot,
1593 language: Option<Arc<Language>>,
1594 language_registry: Option<Arc<LanguageRegistry>>,
1595 cx: &mut Context<Self>,
1596 ) -> Self {
1597 let base_text = buffer.text();
1598 let base_text = cx.new(|cx| {
1599 let mut base_buffer = language::Buffer::local(base_text, cx);
1600 base_buffer.set_capability(Capability::ReadOnly, cx);
1601 if let Some(language_registry) = language_registry {
1602 base_buffer.set_language_registry(language_registry);
1603 }
1604 base_buffer.set_language_async(language, cx);
1605 base_buffer
1606 });
1607
1608 let base_text_snapshot = base_text.read(cx).snapshot();
1609
1610 let diff_snapshot = BufferDiffSnapshot {
1611 hunks: SumTree::new(buffer),
1612 pending_hunks: SumTree::new(buffer),
1613 base_text: base_text_snapshot,
1614 base_text_exists: true,
1615 buffer_snapshot: buffer.clone(),
1616 secondary_diff: None,
1617 };
1618
1619 BufferDiff {
1620 buffer_id: buffer.remote_id(),
1621 base_text_buffer: base_text,
1622 diff_snapshot: Some(diff_snapshot),
1623 buffer_snapshot: buffer.clone(),
1624 secondary_diff: None,
1625 }
1626 }
1627
1628 #[cfg(any(test, feature = "test-support"))]
1629 pub fn new_with_base_text(
1630 base_text: &str,
1631 buffer: &text::BufferSnapshot,
1632 cx: &mut Context<Self>,
1633 ) -> Self {
1634 let mut this = BufferDiff::new(buffer, None, None, cx);
1635 let mut base_text = base_text.to_owned();
1636 text::LineEnding::normalize(&mut base_text);
1637 let base_text_buffer = cx.new(|cx| {
1638 let mut buffer = language::Buffer::local(base_text, cx);
1639 buffer.set_capability(Capability::ReadOnly, cx);
1640 buffer
1641 });
1642 let base_text = base_text_buffer.read(cx).snapshot();
1643 this.base_text_buffer = base_text_buffer;
1644 let update = cx.foreground_executor().block_on(this.update_diff(
1645 buffer.clone(),
1646 &base_text,
1647 Some(Arc::from(base_text.text())),
1648 cx,
1649 ));
1650 this.set_snapshot(update, cx);
1651 this
1652 }
1653
1654 pub fn set_secondary_diff(&mut self, diff: Entity<BufferDiff>) {
1655 self.secondary_diff = Some(diff);
1656 }
1657
1658 pub fn secondary_diff(&self) -> Option<Entity<BufferDiff>> {
1659 self.secondary_diff.clone()
1660 }
1661
1662 pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
1663 let Some(diff_snapshot) = &mut self.diff_snapshot else {
1664 return;
1665 };
1666 let Some((first, last)) = diff_snapshot
1667 .pending_hunks
1668 .first()
1669 .zip(diff_snapshot.pending_hunks.last())
1670 else {
1671 return;
1672 };
1673 let changed_range = first.buffer_range.start..last.buffer_range.end;
1674 let base_text_changed_range =
1675 first.diff_base_byte_range.start..last.diff_base_byte_range.end;
1676 let buffer = diff_snapshot.buffer_snapshot.clone();
1677 diff_snapshot.pending_hunks = SumTree::new(&buffer);
1678 cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
1679 changed_range: Some(changed_range.clone()),
1680 base_text_changed_range: Some(base_text_changed_range),
1681 extended_range: Some(changed_range),
1682 base_text_changed: false,
1683 }));
1684 }
1685
1686 /// Installs optimistic pending hunks in this diff, merging them with any
1687 /// existing pending hunks (newest wins on overlap) and emitting a
1688 /// `DiffChanged` covering both the new hunks and any existing pending hunks
1689 /// they replace. `hunks` must be sorted by `buffer_range.start` and
1690 /// non-overlapping.
1691 ///
1692 /// `buffer` must be a current snapshot of this diff's main buffer: the
1693 /// incoming hunks carry anchors minted from the current buffer, which this
1694 /// diff's internal snapshot (from the last settled recalculation) may not
1695 /// have observed yet.
1696 pub fn set_pending_hunks(
1697 &mut self,
1698 hunks: &[PendingHunk],
1699 buffer: &text::BufferSnapshot,
1700 cx: &mut Context<Self>,
1701 ) {
1702 if hunks.is_empty() {
1703 return;
1704 }
1705 let Some(diff_snapshot) = self.diff_snapshot.as_mut() else {
1706 return;
1707 };
1708
1709 let mut new_pending = SumTree::new(buffer);
1710 let mut old = diff_snapshot
1711 .pending_hunks
1712 .cursor::<DiffHunkSummary>(buffer);
1713 let mut changed_start: Option<Anchor> = None;
1714 let mut changed_end: Option<Anchor> = None;
1715 let mut base_start = usize::MAX;
1716 let mut base_end = 0usize;
1717 let mut extend_changed_range = |buffer_range: &Range<Anchor>, base_range: &Range<usize>| {
1718 changed_start = Some(changed_start.map_or(buffer_range.start, |start| {
1719 *start.min(&buffer_range.start, buffer)
1720 }));
1721 changed_end = Some(
1722 changed_end.map_or(buffer_range.end, |end| *end.max(&buffer_range.end, buffer)),
1723 );
1724 base_start = base_start.min(base_range.start);
1725 base_end = base_end.max(base_range.end);
1726 };
1727 for hunk in hunks {
1728 let preceding = old.slice(&hunk.buffer_range.start, Bias::Left);
1729 new_pending.append(preceding, buffer);
1730
1731 // Drop any overlapping or adjacent existing pending hunks, folding
1732 // them into the changed range so that views repaint their full
1733 // extent (a replaced hunk can be wider than its replacement).
1734 while let Some(old_hunk) = old.item() {
1735 if old_hunk
1736 .buffer_range
1737 .start
1738 .cmp(&hunk.buffer_range.end, buffer)
1739 .is_gt()
1740 {
1741 break;
1742 }
1743 extend_changed_range(&old_hunk.buffer_range, &old_hunk.diff_base_byte_range);
1744 old.next();
1745 }
1746
1747 extend_changed_range(&hunk.buffer_range, &hunk.diff_base_byte_range);
1748 new_pending.push(hunk.clone(), buffer);
1749 }
1750 new_pending.append(old.suffix(), buffer);
1751 drop(old);
1752 diff_snapshot.pending_hunks = new_pending;
1753
1754 if let (Some(start), Some(end)) = (changed_start, changed_end) {
1755 let changed_range = Some(start..end);
1756 cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
1757 changed_range: changed_range.clone(),
1758 base_text_changed_range: Some(base_start..base_end),
1759 extended_range: changed_range,
1760 base_text_changed: false,
1761 }));
1762 }
1763 }
1764
1765 /// Optimistically marks every stageable (resp. unstageable) hunk in this diff
1766 /// as staging (resp. unstaging). Used by whole-file staging from the git
1767 /// panel, where the actual index change is performed by `git add`/`reset`
1768 /// rather than the optimistic index patch.
1769 pub fn mark_all_hunks_pending(
1770 &mut self,
1771 stage: bool,
1772 buffer: &text::BufferSnapshot,
1773 cx: &mut Context<Self>,
1774 ) {
1775 let sense = PendingSense::SetSecondaryStatus { stage };
1776 let version = buffer.version().clone();
1777 let hunks = self
1778 .snapshot(cx)
1779 .hunks_intersecting_range(Anchor::min_max_range_for_buffer(buffer.remote_id()), buffer)
1780 .filter(|hunk| {
1781 !((stage && hunk.secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
1782 || (!stage
1783 && hunk.secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk))
1784 })
1785 .map(|hunk| {
1786 PendingHunk::new(
1787 hunk.buffer_range,
1788 hunk.diff_base_byte_range,
1789 version.clone(),
1790 sense,
1791 )
1792 })
1793 .collect::<Vec<_>>();
1794 self.set_pending_hunks(&hunks, buffer, cx);
1795 }
1796
1797 pub fn suppress_all_hunks_pending(
1798 &mut self,
1799 buffer: &text::BufferSnapshot,
1800 cx: &mut Context<Self>,
1801 ) {
1802 let version = buffer.version().clone();
1803 let hunks = self
1804 .snapshot(cx)
1805 .raw_hunks_intersecting_range(
1806 Anchor::min_max_range_for_buffer(buffer.remote_id()),
1807 buffer,
1808 )
1809 .map(|hunk| {
1810 PendingHunk::new(
1811 hunk.buffer_range,
1812 hunk.diff_base_byte_range,
1813 version.clone(),
1814 PendingSense::Suppress,
1815 )
1816 })
1817 .collect::<Vec<_>>();
1818 self.set_pending_hunks(&hunks, buffer, cx);
1819 }
1820
1821 /// Computes the index-text edits for unstaging the given staged (HEAD-vs-index)
1822 /// hunks. `index_buffer` is this diff's main buffer (the index text). The
1823 /// returned edits are in index coordinates.
1824 pub fn unstage_staged_hunks(
1825 &self,
1826 hunks: &[DiffHunk],
1827 index_buffer: &text::BufferSnapshot,
1828 ) -> Option<Vec<(Range<usize>, Arc<str>)>> {
1829 let Some(diff_snapshot) = self.diff_snapshot.as_ref() else {
1830 return Some(Vec::new());
1831 };
1832 // With no HEAD, the whole file is one staged addition; unstaging it
1833 // removes the file from the index entirely.
1834 if !diff_snapshot.base_text_exists {
1835 return None;
1836 }
1837 let head_text = diff_snapshot.base_text.as_rope();
1838 let mut edits = hunks
1839 .iter()
1840 .map(|hunk| {
1841 let index_range = hunk.buffer_range.to_offset(index_buffer);
1842 let replacement_text: Arc<str> = Arc::from(
1843 head_text
1844 .chunks_in_range(hunk.diff_base_byte_range.clone())
1845 .collect::<String>(),
1846 );
1847 (index_range, replacement_text)
1848 })
1849 .collect::<Vec<_>>();
1850 edits.sort_by_key(|(range, _)| range.start);
1851 Some(edits)
1852 }
1853
1854 #[cfg(any(test, feature = "test-support"))]
1855 pub fn stage_or_unstage_hunks(
1856 &mut self,
1857 stage: bool,
1858 hunks: &[DiffHunk],
1859 buffer: &text::BufferSnapshot,
1860 file_exists: bool,
1861 cx: &mut Context<Self>,
1862 ) -> Option<Rope> {
1863 let secondary_diff = self.secondary_diff.clone()?;
1864 let unstaged_diff_snapshot = secondary_diff.read_with(cx, |secondary_diff, _cx| {
1865 secondary_diff.diff_snapshot.clone()
1866 })?;
1867 let diff_snapshot = self.diff_snapshot.clone()?;
1868 let (edits, pending) = diff_snapshot.compute_uncommitted_index_edits(
1869 &unstaged_diff_snapshot,
1870 stage,
1871 hunks,
1872 buffer,
1873 file_exists,
1874 );
1875 self.set_pending_hunks(&pending, buffer, cx);
1876 edits.map(|edits| {
1877 let mut index_text = unstaged_diff_snapshot.base_text.as_rope().clone();
1878 for (old_range, replacement_text) in edits.iter().rev() {
1879 index_text.replace(old_range.clone(), replacement_text);
1880 }
1881 index_text
1882 })
1883 }
1884
1885 #[cfg(any(test, feature = "test-support"))]
1886 pub fn stage_or_unstage_all_hunks(
1887 &mut self,
1888 stage: bool,
1889 buffer: &text::BufferSnapshot,
1890 file_exists: bool,
1891 cx: &mut Context<Self>,
1892 ) -> Option<Rope> {
1893 let hunks = self
1894 .snapshot(cx)
1895 .hunks_intersecting_range(Anchor::min_max_range_for_buffer(buffer.remote_id()), buffer)
1896 .collect::<Vec<_>>();
1897 self.stage_or_unstage_hunks(stage, &hunks, buffer, file_exists, cx)
1898 }
1899
1900 pub fn update_diff(
1901 &self,
1902 buffer: text::BufferSnapshot,
1903 base_text_snapshot: &language::BufferSnapshot,
1904 base_text: Option<Arc<str>>,
1905 cx: &App,
1906 ) -> Task<BufferDiffUpdate> {
1907 let base_text = base_text.map(|t| text::LineEnding::normalize_arc(t));
1908 debug_assert_eq!(
1909 base_text.as_deref().unwrap_or_default(),
1910 &base_text_snapshot.text()
1911 );
1912 debug_assert_eq!(
1913 base_text_snapshot.remote_id(),
1914 self.base_text_buffer.read(cx).remote_id()
1915 );
1916
1917 let language = base_text_snapshot.language();
1918 let diff_options = build_diff_options(
1919 language.map(|l| l.name()),
1920 language.map(|l| l.default_scope()),
1921 cx,
1922 );
1923 let buffer_snapshot = buffer.clone();
1924 let base_text_snapshot = base_text_snapshot.clone();
1925 let base_text_exists = base_text.is_some();
1926 let unchanged_hunks = self.diff_snapshot.as_ref().and_then(|diff_snapshot| {
1927 if diff_snapshot.base_text_exists == base_text_exists
1928 && diff_snapshot.base_text.version() == base_text_snapshot.version()
1929 && diff_snapshot.buffer_snapshot.version() == buffer_snapshot.version()
1930 {
1931 Some(diff_snapshot.hunks.clone())
1932 } else {
1933 None
1934 }
1935 });
1936
1937 cx.background_executor().spawn(async move {
1938 let hunks = if let Some(unchanged_hunks) = unchanged_hunks {
1939 unchanged_hunks
1940 } else if let Some(base_text) = base_text {
1941 compute_hunks(
1942 Some((base_text, base_text_snapshot.as_rope().clone())),
1943 &buffer,
1944 diff_options,
1945 )
1946 } else {
1947 compute_hunks(None, &buffer, diff_options)
1948 };
1949
1950 BufferDiffUpdate {
1951 hunks,
1952 base_text: base_text_snapshot,
1953 base_text_exists,
1954 buffer_snapshot,
1955 }
1956 })
1957 }
1958
1959 pub fn set_snapshot_with_secondary(
1960 &mut self,
1961 update: BufferDiffUpdate,
1962 secondary_diff_change: Option<Range<Anchor>>,
1963 clear_pending_hunks: bool,
1964 cx: &mut Context<Self>,
1965 ) -> Option<Range<Anchor>> {
1966 log::debug!("set snapshot with secondary {secondary_diff_change:?}");
1967
1968 let BufferDiffUpdate {
1969 hunks: new_hunks,
1970 base_text: new_base_text,
1971 base_text_exists: new_base_text_exists,
1972 buffer_snapshot: new_buffer_snapshot,
1973 } = update;
1974 let buffer = &new_buffer_snapshot;
1975 let old_snapshot = self
1976 .diff_snapshot
1977 .clone()
1978 .unwrap_or_else(|| BufferDiffSnapshot {
1979 hunks: SumTree::new(buffer),
1980 pending_hunks: SumTree::new(buffer),
1981 base_text: new_base_text.clone(),
1982 base_text_exists: false,
1983 buffer_snapshot: new_buffer_snapshot.clone(),
1984 secondary_diff: None,
1985 });
1986 let mut new_snapshot = BufferDiffSnapshot {
1987 hunks: new_hunks.clone(),
1988 base_text: new_base_text.clone(),
1989 base_text_exists: new_base_text_exists,
1990 buffer_snapshot: new_buffer_snapshot.clone(),
1991 pending_hunks: old_snapshot.pending_hunks.clone(),
1992 secondary_diff: None,
1993 };
1994
1995 let old_base_text_exists = old_snapshot.base_text_exists;
1996 let old_buffer_snapshot = &old_snapshot.buffer_snapshot;
1997 let old_base_text = &old_snapshot.base_text;
1998 let base_text_changed = old_base_text_exists != new_base_text_exists
1999 || (new_base_text_exists
2000 && (old_base_text.remote_id() != new_base_text.remote_id()
2001 || new_base_text
2002 .version()
2003 .changed_since(old_base_text.version())));
2004 let DiffChanged {
2005 mut changed_range,
2006 mut base_text_changed_range,
2007 mut extended_range,
2008 base_text_changed: _,
2009 } = match (old_base_text_exists, new_base_text_exists) {
2010 (false, false) if self.diff_snapshot.is_some() => DiffChanged::default(),
2011 (true, true) => compare_hunks(
2012 &new_hunks,
2013 &old_snapshot.hunks,
2014 old_buffer_snapshot,
2015 buffer,
2016 old_base_text,
2017 &new_base_text,
2018 ),
2019 _ => {
2020 let full_range = text::Anchor::min_max_range_for_buffer(self.buffer_id);
2021 let full_base_range = 0..new_base_text.len();
2022 DiffChanged {
2023 changed_range: Some(full_range.clone()),
2024 base_text_changed_range: Some(full_base_range),
2025 extended_range: Some(full_range),
2026 base_text_changed: false,
2027 }
2028 }
2029 };
2030
2031 if base_text_changed || clear_pending_hunks {
2032 if let Some((first, last)) = old_snapshot
2033 .pending_hunks
2034 .first()
2035 .zip(old_snapshot.pending_hunks.last())
2036 {
2037 let pending_range = first.buffer_range.start..last.buffer_range.end;
2038 if let Some(range) = &mut changed_range {
2039 range.start = *range.start.min(&pending_range.start, buffer);
2040 range.end = *range.end.max(&pending_range.end, buffer);
2041 } else {
2042 changed_range = Some(pending_range.clone());
2043 }
2044
2045 if let Some(base_text_range) = base_text_changed_range.as_mut() {
2046 base_text_range.start =
2047 base_text_range.start.min(first.diff_base_byte_range.start);
2048 base_text_range.end = base_text_range.end.max(last.diff_base_byte_range.end);
2049 } else {
2050 base_text_changed_range =
2051 Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end);
2052 }
2053
2054 if let Some(ext) = &mut extended_range {
2055 ext.start = *ext.start.min(&pending_range.start, buffer);
2056 ext.end = *ext.end.max(&pending_range.end, buffer);
2057 } else {
2058 extended_range = Some(pending_range);
2059 }
2060 }
2061 new_snapshot.pending_hunks = SumTree::new(buffer);
2062 }
2063
2064 if let Some(secondary_changed_range) = secondary_diff_change
2065 && let (Some(secondary_hunk_range), Some(secondary_base_range)) =
2066 old_snapshot.range_to_hunk_range(secondary_changed_range, buffer)
2067 {
2068 if let Some(range) = &mut changed_range {
2069 range.start = *secondary_hunk_range.start.min(&range.start, buffer);
2070 range.end = *secondary_hunk_range.end.max(&range.end, buffer);
2071 } else {
2072 changed_range = Some(secondary_hunk_range.clone());
2073 }
2074
2075 if let Some(base_text_range) = base_text_changed_range.as_mut() {
2076 base_text_range.start = secondary_base_range.start.min(base_text_range.start);
2077 base_text_range.end = secondary_base_range.end.max(base_text_range.end);
2078 } else {
2079 base_text_changed_range = Some(secondary_base_range);
2080 }
2081
2082 if let Some(ext) = &mut extended_range {
2083 ext.start = *ext.start.min(&secondary_hunk_range.start, buffer);
2084 ext.end = *ext.end.max(&secondary_hunk_range.end, buffer);
2085 } else {
2086 extended_range = Some(secondary_hunk_range);
2087 }
2088 }
2089
2090 self.diff_snapshot = Some(new_snapshot);
2091 self.buffer_snapshot = new_buffer_snapshot;
2092
2093 let result = DiffChanged {
2094 changed_range,
2095 base_text_changed_range,
2096 extended_range,
2097 base_text_changed,
2098 };
2099 if result.base_text_changed {
2100 cx.emit(BufferDiffEvent::BaseTextChanged);
2101 }
2102 let changed_range = result.changed_range.clone();
2103 cx.emit(BufferDiffEvent::DiffChanged(result));
2104 changed_range
2105 }
2106
2107 pub fn set_snapshot(
2108 &mut self,
2109 new_state: BufferDiffUpdate,
2110 cx: &mut Context<Self>,
2111 ) -> Option<Range<Anchor>> {
2112 self.set_snapshot_with_secondary(new_state, None, false, cx)
2113 }
2114
2115 pub fn base_text(&self, cx: &App) -> language::BufferSnapshot {
2116 self.base_text_buffer.read(cx).snapshot()
2117 }
2118
2119 pub fn base_text_exists(&self) -> bool {
2120 self.diff_snapshot
2121 .as_ref()
2122 .is_some_and(|diff_snapshot| diff_snapshot.base_text_exists)
2123 }
2124
2125 pub fn changed_row_counts(&self) -> (u32, u32) {
2126 self.diff_snapshot
2127 .as_ref()
2128 .map_or((0, 0), |diff_snapshot| diff_snapshot.changed_row_counts())
2129 }
2130
2131 pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot {
2132 let mut snapshot = self.diff_snapshot.clone().unwrap_or_else(|| {
2133 let base_text = self.base_text_buffer.read(cx).snapshot();
2134 BufferDiffSnapshot {
2135 hunks: SumTree::new(&self.buffer_snapshot),
2136 pending_hunks: SumTree::new(&self.buffer_snapshot),
2137 base_text,
2138 base_text_exists: false,
2139 buffer_snapshot: self.buffer_snapshot.clone(),
2140 secondary_diff: None,
2141 }
2142 });
2143 snapshot.secondary_diff = self.secondary_diff.as_ref().map(|diff| {
2144 debug_assert!(diff.read(cx).secondary_diff.is_none());
2145 Arc::new(diff.read(cx).snapshot(cx))
2146 });
2147 snapshot
2148 }
2149
2150 /// Used in cases where the change set isn't derived from git.
2151 ///
2152 /// Dropping the returned task cancels the update, leaving the diff
2153 /// unchanged. Calls must not overlap; to re-run this when the buffer or
2154 /// base text changes, store the task somewhere that the next call will
2155 /// overwrite, so that the previous call is cancelled.
2156 pub fn set_base_text(
2157 &mut self,
2158 base_text: Option<Arc<str>>,
2159 buffer: text::BufferSnapshot,
2160 cx: &mut Context<Self>,
2161 ) -> Task<()> {
2162 cx.spawn(async move |this, cx| {
2163 let base_text_exists = base_text.is_some();
2164 let base_text = base_text.unwrap_or_default();
2165 let Some(base_text_diff) = this
2166 .update(cx, |this, cx| {
2167 this.base_text_buffer.update(cx, |base_text_buffer, cx| {
2168 base_text_buffer.diff(base_text.clone(), cx)
2169 })
2170 })
2171 .log_err()
2172 else {
2173 return;
2174 };
2175 let base_text_diff = base_text_diff.await;
2176 let Some(edited_base_text) = this
2177 .update(cx, |this, cx| {
2178 if this.base_text_buffer.read(cx).version() != base_text_diff.base_version {
2179 log::warn!("dropping concurrent diff update");
2180 debug_panic!("incorrect concurrent call to set_base_text");
2181 return None;
2182 }
2183 let edited_base_text =
2184 this.base_text_buffer.update(cx, |base_text_buffer, cx| {
2185 base_text_buffer.set_line_ending(base_text_diff.line_ending, cx);
2186 assert!(base_text_buffer.version() == base_text_diff.base_version);
2187 base_text_buffer.snapshot_with_edits(base_text_diff.edits, cx)
2188 });
2189 Some(edited_base_text)
2190 })
2191 .log_err()
2192 .flatten()
2193 else {
2194 return;
2195 };
2196 let edited_base_text = edited_base_text.await;
2197 let base_text_snapshot = edited_base_text.snapshot().clone();
2198 let Some(state) = this
2199 .update(cx, |this, cx| {
2200 this.update_diff(
2201 buffer.clone(),
2202 &base_text_snapshot,
2203 base_text_exists.then(|| base_text.clone()),
2204 cx,
2205 )
2206 })
2207 .log_err()
2208 else {
2209 return;
2210 };
2211 let state = state.await;
2212 this.update(cx, |this, cx| {
2213 if &this.base_text_buffer.read(cx).version() != edited_base_text.base_version() {
2214 log::warn!("dropping concurrent diff update");
2215 debug_panic!("incorrect concurrent call to set_base_text");
2216 return;
2217 }
2218
2219 this.base_text_buffer.update(cx, |base_text_buffer, cx| {
2220 base_text_buffer.fast_forward(edited_base_text, cx)
2221 });
2222 this.set_snapshot(state, cx);
2223 })
2224 .log_err();
2225 })
2226 }
2227
2228 pub fn base_text_string(&self, _cx: &App) -> Option<String> {
2229 self.diff_snapshot.as_ref().and_then(|diff_snapshot| {
2230 if diff_snapshot.base_text_exists {
2231 Some(diff_snapshot.base_text.text())
2232 } else {
2233 None
2234 }
2235 })
2236 }
2237
2238 #[cfg(any(test, feature = "test-support"))]
2239 pub fn recalculate_diff_sync(&mut self, buffer: &text::BufferSnapshot, cx: &mut Context<Self>) {
2240 let base_text = self.base_text(cx);
2241 let fut = self.update_diff(
2242 buffer.clone(),
2243 &base_text,
2244 self.base_text_exists().then(|| Arc::from(base_text.text())),
2245 cx,
2246 );
2247 let fg_executor = cx.foreground_executor().clone();
2248 let snapshot = fg_executor.block_on(fut);
2249 let _changed_range = self.set_snapshot(snapshot, cx);
2250 }
2251
2252 pub fn base_text_buffer(&self) -> &Entity<language::Buffer> {
2253 &self.base_text_buffer
2254 }
2255}
2256
2257impl DiffHunk {
2258 pub fn is_created_file(&self) -> bool {
2259 self.diff_base_byte_range == (0..0)
2260 && self.buffer_range.start.is_min()
2261 && self.buffer_range.end.is_max()
2262 }
2263
2264 pub fn status(&self) -> DiffHunkStatus {
2265 let kind = if self.buffer_range.start == self.buffer_range.end {
2266 DiffHunkStatusKind::Deleted
2267 } else if self.diff_base_byte_range.is_empty() {
2268 DiffHunkStatusKind::Added
2269 } else {
2270 DiffHunkStatusKind::Modified
2271 };
2272 DiffHunkStatus {
2273 kind,
2274 secondary: self.secondary_status,
2275 }
2276 }
2277}
2278
2279impl DiffHunkStatus {
2280 pub fn has_secondary_hunk(&self) -> bool {
2281 matches!(
2282 self.secondary,
2283 DiffHunkSecondaryStatus::HasSecondaryHunk
2284 | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
2285 | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
2286 )
2287 }
2288
2289 pub fn is_pending(&self) -> bool {
2290 matches!(
2291 self.secondary,
2292 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
2293 | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2294 )
2295 }
2296
2297 pub fn is_deleted(&self) -> bool {
2298 self.kind == DiffHunkStatusKind::Deleted
2299 }
2300
2301 pub fn is_added(&self) -> bool {
2302 self.kind == DiffHunkStatusKind::Added
2303 }
2304
2305 pub fn is_modified(&self) -> bool {
2306 self.kind == DiffHunkStatusKind::Modified
2307 }
2308
2309 pub fn added(secondary: DiffHunkSecondaryStatus) -> Self {
2310 Self {
2311 kind: DiffHunkStatusKind::Added,
2312 secondary,
2313 }
2314 }
2315
2316 pub fn modified(secondary: DiffHunkSecondaryStatus) -> Self {
2317 Self {
2318 kind: DiffHunkStatusKind::Modified,
2319 secondary,
2320 }
2321 }
2322
2323 pub fn deleted(secondary: DiffHunkSecondaryStatus) -> Self {
2324 Self {
2325 kind: DiffHunkStatusKind::Deleted,
2326 secondary,
2327 }
2328 }
2329
2330 pub fn deleted_none() -> Self {
2331 Self {
2332 kind: DiffHunkStatusKind::Deleted,
2333 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
2334 }
2335 }
2336
2337 pub fn added_none() -> Self {
2338 Self {
2339 kind: DiffHunkStatusKind::Added,
2340 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
2341 }
2342 }
2343
2344 pub fn modified_none() -> Self {
2345 Self {
2346 kind: DiffHunkStatusKind::Modified,
2347 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
2348 }
2349 }
2350}
2351
2352#[cfg(any(test, feature = "test-support"))]
2353#[track_caller]
2354pub fn assert_hunks<ExpectedText, HunkIter>(
2355 diff_hunks: HunkIter,
2356 buffer: &text::BufferSnapshot,
2357 diff_base: &str,
2358 // Line range, deleted, added, status
2359 expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
2360) where
2361 HunkIter: Iterator<Item = DiffHunk>,
2362 ExpectedText: AsRef<str>,
2363{
2364 let actual_hunks = diff_hunks
2365 .map(|hunk| {
2366 (
2367 hunk.range.clone(),
2368 &diff_base[hunk.diff_base_byte_range.clone()],
2369 buffer
2370 .text_for_range(hunk.range.clone())
2371 .collect::<String>(),
2372 hunk.status(),
2373 )
2374 })
2375 .collect::<Vec<_>>();
2376
2377 let expected_hunks: Vec<_> = expected_hunks
2378 .iter()
2379 .map(|(line_range, deleted_text, added_text, status)| {
2380 (
2381 Point::new(line_range.start, 0)..Point::new(line_range.end, 0),
2382 deleted_text.as_ref(),
2383 added_text.as_ref().to_string(),
2384 *status,
2385 )
2386 })
2387 .collect();
2388
2389 pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
2390}
2391
2392#[cfg(test)]
2393mod tests {
2394 use std::{fmt::Write as _, sync::mpsc};
2395
2396 use super::*;
2397 use gpui::TestAppContext;
2398 use pretty_assertions::{assert_eq, assert_ne};
2399 use rand::{Rng as _, rngs::StdRng};
2400 use text::{Buffer, BufferId, ReplicaId, Rope};
2401 use unindent::Unindent as _;
2402 use util::test::marked_text_ranges;
2403
2404 #[ctor::ctor(unsafe)]
2405 fn init_logger() {
2406 zlog::init_test();
2407 }
2408
2409 #[gpui::test]
2410 async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
2411 let diff_base = "
2412 one
2413 two
2414 three
2415 "
2416 .unindent();
2417
2418 let buffer_text = "
2419 one
2420 HELLO
2421 three
2422 "
2423 .unindent();
2424
2425 let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2426 let mut diff = BufferDiffSnapshot::new_sync(&buffer, diff_base.clone(), cx);
2427 assert_hunks(
2428 diff.hunks_intersecting_range(
2429 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2430 &buffer,
2431 ),
2432 &buffer,
2433 &diff_base,
2434 &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
2435 );
2436
2437 buffer.edit([(0..0, "point five\n")]);
2438 diff = BufferDiffSnapshot::new_sync(&buffer, diff_base.clone(), cx);
2439 assert_hunks(
2440 diff.hunks_intersecting_range(
2441 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2442 &buffer,
2443 ),
2444 &buffer,
2445 &diff_base,
2446 &[
2447 (0..1, "", "point five\n", DiffHunkStatus::added_none()),
2448 (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()),
2449 ],
2450 );
2451
2452 diff = cx.update(|cx| BufferDiff::new(&buffer, None, None, cx).snapshot(cx));
2453 assert_hunks::<&str, _>(
2454 diff.hunks_intersecting_range(
2455 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2456 &buffer,
2457 ),
2458 &buffer,
2459 &diff_base,
2460 &[],
2461 );
2462 }
2463
2464 #[gpui::test]
2465 async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
2466 let head_text = "
2467 zero
2468 one
2469 two
2470 three
2471 four
2472 five
2473 six
2474 seven
2475 eight
2476 nine
2477 "
2478 .unindent();
2479
2480 let index_text = "
2481 zero
2482 one
2483 TWO
2484 three
2485 FOUR
2486 five
2487 six
2488 seven
2489 eight
2490 NINE
2491 "
2492 .unindent();
2493
2494 let buffer_text = "
2495 zero
2496 one
2497 TWO
2498 three
2499 FOUR
2500 FIVE
2501 six
2502 SEVEN
2503 eight
2504 nine
2505 "
2506 .unindent();
2507
2508 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2509 let unstaged_diff = BufferDiffSnapshot::new_sync(&buffer, index_text, cx);
2510 let mut uncommitted_diff = BufferDiffSnapshot::new_sync(&buffer, head_text.clone(), cx);
2511 uncommitted_diff.secondary_diff = Some(Arc::new(unstaged_diff));
2512
2513 let expected_hunks = vec![
2514 (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
2515 (
2516 4..6,
2517 "four\nfive\n",
2518 "FOUR\nFIVE\n",
2519 DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
2520 ),
2521 (
2522 7..8,
2523 "seven\n",
2524 "SEVEN\n",
2525 DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
2526 ),
2527 ];
2528
2529 assert_hunks(
2530 uncommitted_diff.hunks_intersecting_range(
2531 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2532 &buffer,
2533 ),
2534 &buffer,
2535 &head_text,
2536 &expected_hunks,
2537 );
2538 }
2539
2540 #[gpui::test]
2541 async fn test_buffer_diff_range(cx: &mut TestAppContext) {
2542 let diff_base = "
2543 one
2544 two
2545 three
2546 four
2547 five
2548 six
2549 seven
2550 eight
2551 nine
2552 ten
2553 "
2554 .unindent();
2555
2556 let buffer_text = "
2557 A
2558 one
2559 B
2560 two
2561 C
2562 three
2563 HELLO
2564 four
2565 five
2566 SIXTEEN
2567 seven
2568 eight
2569 WORLD
2570 nine
2571
2572 ten
2573
2574 "
2575 .unindent();
2576
2577 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2578 let diff = BufferDiffSnapshot::new_sync(buffer.snapshot(), diff_base.clone(), cx);
2579 assert_eq!(
2580 diff.hunks_intersecting_range(
2581 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2582 &buffer
2583 )
2584 .count(),
2585 8
2586 );
2587
2588 assert_hunks(
2589 diff.hunks_intersecting_range(
2590 buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
2591 &buffer,
2592 ),
2593 &buffer,
2594 &diff_base,
2595 &[
2596 (6..7, "", "HELLO\n", DiffHunkStatus::added_none()),
2597 (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()),
2598 (12..13, "", "WORLD\n", DiffHunkStatus::added_none()),
2599 ],
2600 );
2601 }
2602
2603 #[gpui::test]
2604 async fn test_stage_hunk(cx: &mut TestAppContext) {
2605 struct Example {
2606 name: &'static str,
2607 head_text: String,
2608 index_text: String,
2609 buffer_marked_text: String,
2610 final_index_text: String,
2611 }
2612
2613 let table = [
2614 Example {
2615 name: "uncommitted hunk straddles end of unstaged hunk",
2616 head_text: "
2617 one
2618 two
2619 three
2620 four
2621 five
2622 "
2623 .unindent(),
2624 index_text: "
2625 one
2626 TWO_HUNDRED
2627 three
2628 FOUR_HUNDRED
2629 five
2630 "
2631 .unindent(),
2632 buffer_marked_text: "
2633 ZERO
2634 one
2635 two
2636 «THREE_HUNDRED
2637 FOUR_HUNDRED»
2638 five
2639 SIX
2640 "
2641 .unindent(),
2642 final_index_text: "
2643 one
2644 two
2645 THREE_HUNDRED
2646 FOUR_HUNDRED
2647 five
2648 "
2649 .unindent(),
2650 },
2651 Example {
2652 name: "uncommitted hunk straddles start of unstaged hunk",
2653 head_text: "
2654 one
2655 two
2656 three
2657 four
2658 five
2659 "
2660 .unindent(),
2661 index_text: "
2662 one
2663 TWO_HUNDRED
2664 three
2665 FOUR_HUNDRED
2666 five
2667 "
2668 .unindent(),
2669 buffer_marked_text: "
2670 ZERO
2671 one
2672 «TWO_HUNDRED
2673 THREE_HUNDRED»
2674 four
2675 five
2676 SIX
2677 "
2678 .unindent(),
2679 final_index_text: "
2680 one
2681 TWO_HUNDRED
2682 THREE_HUNDRED
2683 four
2684 five
2685 "
2686 .unindent(),
2687 },
2688 Example {
2689 name: "uncommitted hunk strictly contains unstaged hunks",
2690 head_text: "
2691 one
2692 two
2693 three
2694 four
2695 five
2696 six
2697 seven
2698 "
2699 .unindent(),
2700 index_text: "
2701 one
2702 TWO
2703 THREE
2704 FOUR
2705 FIVE
2706 SIX
2707 seven
2708 "
2709 .unindent(),
2710 buffer_marked_text: "
2711 one
2712 TWO
2713 «THREE_HUNDRED
2714 FOUR
2715 FIVE_HUNDRED»
2716 SIX
2717 seven
2718 "
2719 .unindent(),
2720 final_index_text: "
2721 one
2722 TWO
2723 THREE_HUNDRED
2724 FOUR
2725 FIVE_HUNDRED
2726 SIX
2727 seven
2728 "
2729 .unindent(),
2730 },
2731 Example {
2732 name: "uncommitted deletion hunk",
2733 head_text: "
2734 one
2735 two
2736 three
2737 four
2738 five
2739 "
2740 .unindent(),
2741 index_text: "
2742 one
2743 two
2744 three
2745 four
2746 five
2747 "
2748 .unindent(),
2749 buffer_marked_text: "
2750 one
2751 ˇfive
2752 "
2753 .unindent(),
2754 final_index_text: "
2755 one
2756 five
2757 "
2758 .unindent(),
2759 },
2760 Example {
2761 name: "one unstaged hunk that contains two uncommitted hunks",
2762 head_text: "
2763 one
2764 two
2765
2766 three
2767 four
2768 "
2769 .unindent(),
2770 index_text: "
2771 one
2772 two
2773 three
2774 four
2775 "
2776 .unindent(),
2777 buffer_marked_text: "
2778 «one
2779
2780 three // modified
2781 four»
2782 "
2783 .unindent(),
2784 final_index_text: "
2785 one
2786
2787 three // modified
2788 four
2789 "
2790 .unindent(),
2791 },
2792 Example {
2793 name: "one uncommitted hunk that contains two unstaged hunks",
2794 head_text: "
2795 one
2796 two
2797 three
2798 four
2799 five
2800 "
2801 .unindent(),
2802 index_text: "
2803 ZERO
2804 one
2805 TWO
2806 THREE
2807 FOUR
2808 five
2809 "
2810 .unindent(),
2811 buffer_marked_text: "
2812 «one
2813 TWO_HUNDRED
2814 THREE
2815 FOUR_HUNDRED
2816 five»
2817 "
2818 .unindent(),
2819 final_index_text: "
2820 ZERO
2821 one
2822 TWO_HUNDRED
2823 THREE
2824 FOUR_HUNDRED
2825 five
2826 "
2827 .unindent(),
2828 },
2829 ];
2830
2831 for example in table {
2832 let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
2833 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2834 let hunk_range =
2835 buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
2836
2837 let unstaged_diff =
2838 cx.new(|cx| BufferDiff::new_with_base_text(&example.index_text, &buffer, cx));
2839
2840 let uncommitted_diff = cx.new(|cx| {
2841 let mut diff = BufferDiff::new_with_base_text(&example.head_text, &buffer, cx);
2842 diff.set_secondary_diff(unstaged_diff);
2843 diff
2844 });
2845
2846 uncommitted_diff.update(cx, |diff, cx| {
2847 let hunks = diff
2848 .snapshot(cx)
2849 .hunks_intersecting_range(hunk_range.clone(), &buffer)
2850 .collect::<Vec<_>>();
2851 for hunk in &hunks {
2852 assert_ne!(
2853 hunk.secondary_status,
2854 DiffHunkSecondaryStatus::NoSecondaryHunk
2855 )
2856 }
2857
2858 let new_index_text = diff
2859 .stage_or_unstage_hunks(true, &hunks, &buffer, true, cx)
2860 .unwrap()
2861 .to_string();
2862
2863 let hunks = diff
2864 .snapshot(cx)
2865 .hunks_intersecting_range(hunk_range.clone(), &buffer)
2866 .collect::<Vec<_>>();
2867 for hunk in &hunks {
2868 assert_eq!(
2869 hunk.secondary_status,
2870 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2871 )
2872 }
2873
2874 pretty_assertions::assert_eq!(
2875 new_index_text,
2876 example.final_index_text,
2877 "example: {}",
2878 example.name
2879 );
2880 });
2881 }
2882 }
2883
2884 #[gpui::test]
2885 async fn test_stage_all_with_nested_hunks(cx: &mut TestAppContext) {
2886 // This test reproduces a crash where staging all hunks would cause an underflow
2887 // when there's one large unstaged hunk containing multiple uncommitted hunks.
2888 let head_text = "
2889 aaa
2890 bbb
2891 ccc
2892 ddd
2893 eee
2894 fff
2895 ggg
2896 hhh
2897 iii
2898 jjj
2899 kkk
2900 lll
2901 "
2902 .unindent();
2903
2904 let index_text = "
2905 aaa
2906 bbb
2907 CCC-index
2908 DDD-index
2909 EEE-index
2910 FFF-index
2911 GGG-index
2912 HHH-index
2913 III-index
2914 JJJ-index
2915 kkk
2916 lll
2917 "
2918 .unindent();
2919
2920 let buffer_text = "
2921 aaa
2922 bbb
2923 ccc-modified
2924 ddd
2925 eee-modified
2926 fff
2927 ggg
2928 hhh-modified
2929 iii
2930 jjj
2931 kkk
2932 lll
2933 "
2934 .unindent();
2935
2936 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2937
2938 let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&index_text, &buffer, cx));
2939 let uncommitted_diff = cx.new(|cx| {
2940 let mut diff = BufferDiff::new_with_base_text(&head_text, &buffer, cx);
2941 diff.set_secondary_diff(unstaged_diff);
2942 diff
2943 });
2944
2945 uncommitted_diff.update(cx, |diff, cx| {
2946 diff.stage_or_unstage_all_hunks(true, &buffer, true, cx);
2947 });
2948 }
2949
2950 #[gpui::test]
2951 async fn test_stage_all_with_stale_buffer(cx: &mut TestAppContext) {
2952 // Regression test for ZED-5R2: when the buffer is edited after the diff is
2953 // computed but before staging, anchor positions shift while diff_base_byte_range
2954 // values don't. If the primary (HEAD) hunk extends past the unstaged (index)
2955 // hunk, an edit in the extension region shifts the primary hunk end without
2956 // shifting the unstaged hunk end. The overshoot calculation then produces an
2957 // index_end that exceeds index_text.len().
2958 //
2959 // Setup:
2960 // HEAD: "aaa\nbbb\nccc\n" (primary hunk covers lines 1-2)
2961 // Index: "aaa\nbbb\nCCC\n" (unstaged hunk covers line 1 only)
2962 // Buffer: "aaa\nBBB\nCCC\n" (both lines differ from HEAD)
2963 //
2964 // The primary hunk spans buffer offsets 4..12, but the unstaged hunk only
2965 // spans 4..8. The pending hunk extends 4 bytes past the unstaged hunk.
2966 // An edit at offset 9 (inside "CCC") shifts the primary hunk end from 12
2967 // to 13 but leaves the unstaged hunk end at 8, making index_end = 13 > 12.
2968 let head_text = "aaa\nbbb\nccc\n";
2969 let index_text = "aaa\nbbb\nCCC\n";
2970 let buffer_text = "aaa\nBBB\nCCC\n";
2971
2972 let mut buffer = Buffer::new(
2973 ReplicaId::LOCAL,
2974 BufferId::new(1).unwrap(),
2975 buffer_text.to_string(),
2976 );
2977
2978 let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(index_text, &buffer, cx));
2979 let uncommitted_diff = cx.new(|cx| {
2980 let mut diff = BufferDiff::new_with_base_text(head_text, &buffer, cx);
2981 diff.set_secondary_diff(unstaged_diff);
2982 diff
2983 });
2984
2985 // Edit the buffer in the region between the unstaged hunk end (offset 8)
2986 // and the primary hunk end (offset 12). This shifts the primary hunk end
2987 // but not the unstaged hunk end.
2988 buffer.edit([(9..9, "Z")]);
2989
2990 uncommitted_diff.update(cx, |diff, cx| {
2991 diff.stage_or_unstage_all_hunks(true, &buffer, true, cx);
2992 });
2993 }
2994
2995 #[gpui::test]
2996 async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
2997 let head_text = "
2998 one
2999 two
3000 three
3001 "
3002 .unindent();
3003 let index_text = head_text.clone();
3004 let buffer_text = "
3005 one
3006 three
3007 "
3008 .unindent();
3009
3010 let buffer = Buffer::new(
3011 ReplicaId::LOCAL,
3012 BufferId::new(1).unwrap(),
3013 buffer_text.clone(),
3014 );
3015 let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&index_text, &buffer, cx));
3016 let uncommitted_diff = cx.new(|cx| {
3017 let mut diff = BufferDiff::new_with_base_text(&head_text, &buffer, cx);
3018 diff.set_secondary_diff(unstaged_diff.clone());
3019 diff
3020 });
3021
3022 uncommitted_diff.update(cx, |diff, cx| {
3023 let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
3024
3025 let new_index_text = diff
3026 .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
3027 .unwrap()
3028 .to_string();
3029 assert_eq!(new_index_text, buffer_text);
3030
3031 let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
3032 assert_eq!(
3033 hunk.secondary_status,
3034 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
3035 );
3036
3037 let index_text = diff
3038 .stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
3039 .unwrap()
3040 .to_string();
3041 assert_eq!(index_text, head_text);
3042
3043 let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
3044 // optimistically unstaged (fine, could also be HasSecondaryHunk)
3045 assert_eq!(
3046 hunk.secondary_status,
3047 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
3048 );
3049 });
3050 }
3051
3052 #[gpui::test]
3053 async fn test_set_pending_hunks_change_covers_replaced_hunks(cx: &mut TestAppContext) {
3054 let base_text = "
3055 zero
3056 one
3057 two
3058 three
3059 four
3060 five
3061 "
3062 .unindent();
3063 let buffer_text = "
3064 ZERO
3065 one
3066 two
3067 THREE
3068 four
3069 FIVE
3070 "
3071 .unindent();
3072 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
3073 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx));
3074
3075 // Install a whole-file pending hunk, as the no-HEAD staging paths do.
3076 let version = buffer.version();
3077 diff.update(cx, |diff, cx| {
3078 diff.set_pending_hunks(
3079 &[PendingHunk::new(
3080 Anchor::min_max_range_for_buffer(buffer.remote_id()),
3081 0..base_text.len(),
3082 version.clone(),
3083 PendingSense::SetSecondaryStatus { stage: true },
3084 )],
3085 &buffer,
3086 cx,
3087 )
3088 });
3089
3090 let (tx, rx) = mpsc::channel();
3091 let subscription =
3092 cx.update(|cx| cx.subscribe(&diff, move |_, event, _| tx.send(event.clone()).unwrap()));
3093
3094 // Replace it with a narrower hunk; the emitted change must still cover
3095 // the whole extent of the replaced hunk.
3096 diff.update(cx, |diff, cx| {
3097 diff.set_pending_hunks(
3098 &[PendingHunk::new(
3099 buffer.anchor_before(Point::new(3, 0))..buffer.anchor_before(Point::new(4, 0)),
3100 base_text.find("three").unwrap()..base_text.find("four").unwrap(),
3101 version,
3102 PendingSense::Suppress,
3103 )],
3104 &buffer,
3105 cx,
3106 )
3107 });
3108
3109 drop(subscription);
3110 let events = rx.into_iter().collect::<Vec<_>>();
3111 match events.as_slice() {
3112 [
3113 BufferDiffEvent::DiffChanged(DiffChanged {
3114 changed_range: Some(changed_range),
3115 ..
3116 }),
3117 ] => {
3118 assert_eq!(
3119 changed_range.to_point(&buffer),
3120 Point::zero()..buffer.max_point(),
3121 );
3122 }
3123 _ => panic!("unexpected events: {:?}", events),
3124 }
3125 }
3126
3127 #[gpui::test]
3128 async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
3129 let base_text = "
3130 zero
3131 one
3132 two
3133 three
3134 four
3135 five
3136 six
3137 seven
3138 eight
3139 nine
3140 "
3141 .unindent();
3142
3143 let buffer_text_1 = "
3144 one
3145 three
3146 four
3147 five
3148 SIX
3149 seven
3150 eight
3151 NINE
3152 "
3153 .unindent();
3154
3155 let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1);
3156
3157 let empty_diff = cx.update(|cx| BufferDiff::new(&buffer, None, None, cx).snapshot(cx));
3158 let diff_1 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
3159 let DiffChanged {
3160 changed_range,
3161 base_text_changed_range,
3162 extended_range: _,
3163 base_text_changed: _,
3164 } = compare_hunks(
3165 &diff_1.hunks,
3166 &empty_diff.hunks,
3167 &buffer,
3168 &buffer,
3169 &diff_1.base_text(),
3170 &diff_1.base_text(),
3171 );
3172 let range = changed_range.unwrap();
3173 assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
3174 let base_text_range = base_text_changed_range.unwrap();
3175 assert_eq!(
3176 base_text_range.to_point(diff_1.base_text()),
3177 Point::new(0, 0)..Point::new(10, 0)
3178 );
3179
3180 // Edit does affects the diff because it recalculates word diffs.
3181 buffer.edit_via_marked_text(
3182 &"
3183 one
3184 three
3185 four
3186 five
3187 «SIX.5»
3188 seven
3189 eight
3190 NINE
3191 "
3192 .unindent(),
3193 );
3194 let diff_2 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
3195 let DiffChanged {
3196 changed_range,
3197 base_text_changed_range,
3198 extended_range: _,
3199 base_text_changed: _,
3200 } = compare_hunks(
3201 &diff_2.hunks,
3202 &diff_1.hunks,
3203 &buffer,
3204 &buffer,
3205 diff_2.base_text(),
3206 diff_2.base_text(),
3207 );
3208 assert_eq!(
3209 changed_range.unwrap().to_point(&buffer),
3210 Point::new(4, 0)..Point::new(5, 0),
3211 );
3212 assert_eq!(
3213 base_text_changed_range
3214 .unwrap()
3215 .to_point(diff_2.base_text()),
3216 Point::new(6, 0)..Point::new(7, 0),
3217 );
3218
3219 // Edit turns a deletion hunk into a modification.
3220 buffer.edit_via_marked_text(
3221 &"
3222 one
3223 «THREE»
3224 four
3225 five
3226 SIX.5
3227 seven
3228 eight
3229 NINE
3230 "
3231 .unindent(),
3232 );
3233 let diff_3 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
3234 let DiffChanged {
3235 changed_range,
3236 base_text_changed_range,
3237 extended_range: _,
3238 base_text_changed: _,
3239 } = compare_hunks(
3240 &diff_3.hunks,
3241 &diff_2.hunks,
3242 &buffer,
3243 &buffer,
3244 diff_3.base_text(),
3245 diff_3.base_text(),
3246 );
3247 let range = changed_range.unwrap();
3248 assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
3249 let base_text_range = base_text_changed_range.unwrap();
3250 assert_eq!(
3251 base_text_range.to_point(diff_3.base_text()),
3252 Point::new(2, 0)..Point::new(4, 0)
3253 );
3254
3255 // Edit turns a modification hunk into a deletion.
3256 buffer.edit_via_marked_text(
3257 &"
3258 one
3259 THREE
3260 four
3261 five«»
3262 seven
3263 eight
3264 NINE
3265 "
3266 .unindent(),
3267 );
3268 let diff_4 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
3269 let DiffChanged {
3270 changed_range,
3271 base_text_changed_range,
3272 extended_range: _,
3273 base_text_changed: _,
3274 } = compare_hunks(
3275 &diff_4.hunks,
3276 &diff_3.hunks,
3277 &buffer,
3278 &buffer,
3279 diff_4.base_text(),
3280 diff_4.base_text(),
3281 );
3282 let range = changed_range.unwrap();
3283 assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
3284 let base_text_range = base_text_changed_range.unwrap();
3285 assert_eq!(
3286 base_text_range.to_point(diff_4.base_text()),
3287 Point::new(6, 0)..Point::new(7, 0)
3288 );
3289
3290 // Edit introduces a new insertion hunk.
3291 buffer.edit_via_marked_text(
3292 &"
3293 one
3294 THREE
3295 four«
3296 FOUR.5
3297 »five
3298 seven
3299 eight
3300 NINE
3301 "
3302 .unindent(),
3303 );
3304 let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
3305 let DiffChanged {
3306 changed_range,
3307 base_text_changed_range,
3308 extended_range: _,
3309 base_text_changed: _,
3310 } = compare_hunks(
3311 &diff_5.hunks,
3312 &diff_4.hunks,
3313 &buffer,
3314 &buffer,
3315 diff_5.base_text(),
3316 diff_5.base_text(),
3317 );
3318 let range = changed_range.unwrap();
3319 assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
3320 let base_text_range = base_text_changed_range.unwrap();
3321 assert_eq!(
3322 base_text_range.to_point(diff_5.base_text()),
3323 Point::new(5, 0)..Point::new(5, 0)
3324 );
3325
3326 // Edit removes a hunk.
3327 buffer.edit_via_marked_text(
3328 &"
3329 one
3330 THREE
3331 four
3332 FOUR.5
3333 five
3334 seven
3335 eight
3336 «nine»
3337 "
3338 .unindent(),
3339 );
3340 let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
3341 let DiffChanged {
3342 changed_range,
3343 base_text_changed_range,
3344 extended_range: _,
3345 base_text_changed: _,
3346 } = compare_hunks(
3347 &diff_6.hunks,
3348 &diff_5.hunks,
3349 &buffer,
3350 &buffer,
3351 diff_6.base_text(),
3352 diff_6.base_text(),
3353 );
3354 let range = changed_range.unwrap();
3355 assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
3356 let base_text_range = base_text_changed_range.unwrap();
3357 assert_eq!(
3358 base_text_range.to_point(diff_6.base_text()),
3359 Point::new(9, 0)..Point::new(10, 0)
3360 );
3361
3362 buffer.edit_via_marked_text(
3363 &"
3364 one
3365 THREE
3366 four«»
3367 five
3368 seven
3369 eight
3370 «NINE»
3371 "
3372 .unindent(),
3373 );
3374
3375 let diff_7 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
3376 let DiffChanged {
3377 changed_range,
3378 base_text_changed_range,
3379 extended_range: _,
3380 base_text_changed: _,
3381 } = compare_hunks(
3382 &diff_7.hunks,
3383 &diff_6.hunks,
3384 &buffer,
3385 &buffer,
3386 diff_7.base_text(),
3387 diff_7.base_text(),
3388 );
3389 let range = changed_range.unwrap();
3390 assert_eq!(range.to_point(&buffer), Point::new(2, 4)..Point::new(7, 0));
3391 let base_text_range = base_text_changed_range.unwrap();
3392 assert_eq!(
3393 base_text_range.to_point(diff_7.base_text()),
3394 Point::new(5, 0)..Point::new(10, 0)
3395 );
3396
3397 buffer.edit_via_marked_text(
3398 &"
3399 one
3400 THREE
3401 four
3402 five«»seven
3403 eight
3404 NINE
3405 "
3406 .unindent(),
3407 );
3408
3409 let diff_8 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
3410 let DiffChanged {
3411 changed_range,
3412 base_text_changed_range,
3413 extended_range: _,
3414 base_text_changed: _,
3415 } = compare_hunks(
3416 &diff_8.hunks,
3417 &diff_7.hunks,
3418 &buffer,
3419 &buffer,
3420 diff_8.base_text(),
3421 diff_8.base_text(),
3422 );
3423 let range = changed_range.unwrap();
3424 assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(3, 4));
3425 let base_text_range = base_text_changed_range.unwrap();
3426 assert_eq!(
3427 base_text_range.to_point(diff_8.base_text()),
3428 Point::new(5, 0)..Point::new(8, 0)
3429 );
3430 }
3431
3432 #[gpui::test(iterations = 100)]
3433 async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
3434 fn gen_line(rng: &mut StdRng) -> String {
3435 if rng.random_bool(0.2) {
3436 "\n".to_owned()
3437 } else {
3438 let c = rng.random_range('A'..='Z');
3439 format!("{c}{c}{c}\n")
3440 }
3441 }
3442
3443 fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
3444 let mut old_lines = {
3445 let mut old_lines = Vec::new();
3446 let old_lines_iter = head.lines();
3447 for line in old_lines_iter {
3448 assert!(!line.ends_with("\n"));
3449 old_lines.push(line.to_owned());
3450 }
3451 if old_lines.last().is_some_and(|line| line.is_empty()) {
3452 old_lines.pop();
3453 }
3454 old_lines.into_iter()
3455 };
3456 let mut result = String::new();
3457 let unchanged_count = rng.random_range(0..=old_lines.len());
3458 result +=
3459 &old_lines
3460 .by_ref()
3461 .take(unchanged_count)
3462 .fold(String::new(), |mut s, line| {
3463 writeln!(&mut s, "{line}").unwrap();
3464 s
3465 });
3466 while old_lines.len() > 0 {
3467 let deleted_count = rng.random_range(0..=old_lines.len());
3468 let _advance = old_lines
3469 .by_ref()
3470 .take(deleted_count)
3471 .map(|line| line.len() + 1)
3472 .sum::<usize>();
3473 let minimum_added = if deleted_count == 0 { 1 } else { 0 };
3474 let added_count = rng.random_range(minimum_added..=5);
3475 let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
3476 result += &addition;
3477
3478 if old_lines.len() > 0 {
3479 let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
3480 if blank_lines == old_lines.len() {
3481 break;
3482 };
3483 let unchanged_count =
3484 rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
3485 result += &old_lines.by_ref().take(unchanged_count).fold(
3486 String::new(),
3487 |mut s, line| {
3488 writeln!(&mut s, "{line}").unwrap();
3489 s
3490 },
3491 );
3492 }
3493 }
3494 result
3495 }
3496
3497 fn uncommitted_diff(
3498 working_copy: &language::BufferSnapshot,
3499 index_text: &Rope,
3500 head_text: String,
3501 cx: &mut TestAppContext,
3502 ) -> Entity<BufferDiff> {
3503 let secondary = cx.new(|cx| {
3504 BufferDiff::new_with_base_text(&index_text.to_string(), &working_copy.text, cx)
3505 });
3506 cx.new(|cx| {
3507 let mut diff = BufferDiff::new_with_base_text(&head_text, &working_copy.text, cx);
3508 diff.secondary_diff = Some(secondary);
3509 diff
3510 })
3511 }
3512
3513 let operations = std::env::var("OPERATIONS")
3514 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
3515 .unwrap_or(10);
3516
3517 let rng = &mut rng;
3518 let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
3519 writeln!(&mut s, "{c}{c}{c}").unwrap();
3520 s
3521 });
3522 let working_copy = gen_working_copy(rng, &head_text);
3523 let working_copy = cx.new(|cx| {
3524 language::Buffer::local_normalized(
3525 Rope::from(working_copy.as_str()),
3526 text::LineEnding::default(),
3527 cx,
3528 )
3529 });
3530 let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
3531 let mut index_text = if rng.random() {
3532 Rope::from(head_text.as_str())
3533 } else {
3534 working_copy.as_rope().clone()
3535 };
3536
3537 let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
3538 let mut hunks = diff.update(cx, |diff, cx| {
3539 diff.snapshot(cx)
3540 .hunks_intersecting_range(
3541 Anchor::min_max_range_for_buffer(diff.buffer_id),
3542 &working_copy,
3543 )
3544 .collect::<Vec<_>>()
3545 });
3546 if hunks.is_empty() {
3547 return;
3548 }
3549
3550 for _ in 0..operations {
3551 let i = rng.random_range(0..hunks.len());
3552 let hunk = &mut hunks[i];
3553 let hunk_to_change = hunk.clone();
3554 let stage = match hunk.secondary_status {
3555 DiffHunkSecondaryStatus::HasSecondaryHunk => {
3556 hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
3557 true
3558 }
3559 DiffHunkSecondaryStatus::NoSecondaryHunk => {
3560 hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
3561 false
3562 }
3563 _ => unreachable!(),
3564 };
3565
3566 index_text = diff.update(cx, |diff, cx| {
3567 diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx)
3568 .unwrap()
3569 });
3570
3571 diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
3572 let found_hunks = diff.update(cx, |diff, cx| {
3573 diff.snapshot(cx)
3574 .hunks_intersecting_range(
3575 Anchor::min_max_range_for_buffer(diff.buffer_id),
3576 &working_copy,
3577 )
3578 .collect::<Vec<_>>()
3579 });
3580 assert_eq!(hunks.len(), found_hunks.len());
3581
3582 for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
3583 assert_eq!(
3584 expected_hunk.buffer_range.to_point(&working_copy),
3585 found_hunk.buffer_range.to_point(&working_copy)
3586 );
3587 assert_eq!(
3588 expected_hunk.diff_base_byte_range,
3589 found_hunk.diff_base_byte_range
3590 );
3591 assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
3592 }
3593 hunks = found_hunks;
3594 }
3595 }
3596
3597 #[gpui::test]
3598 async fn test_changed_ranges(cx: &mut gpui::TestAppContext) {
3599 let base_text = "
3600 one
3601 two
3602 three
3603 four
3604 five
3605 six
3606 "
3607 .unindent();
3608 let buffer_text = "
3609 one
3610 TWO
3611 three
3612 four
3613 FIVE
3614 six
3615 "
3616 .unindent();
3617 let buffer = cx.new(|cx| language::Buffer::local(buffer_text, cx));
3618 let diff = cx.new(|cx| {
3619 BufferDiff::new_with_base_text(&base_text, &buffer.read(cx).text_snapshot(), cx)
3620 });
3621 cx.run_until_parked();
3622 let (tx, rx) = mpsc::channel();
3623 let subscription =
3624 cx.update(|cx| cx.subscribe(&diff, move |_, event, _| tx.send(event.clone()).unwrap()));
3625
3626 let snapshot = buffer.update(cx, |buffer, cx| {
3627 buffer.set_text(
3628 "
3629 ONE
3630 TWO
3631 THREE
3632 FOUR
3633 FIVE
3634 SIX
3635 "
3636 .unindent(),
3637 cx,
3638 );
3639 buffer.text_snapshot()
3640 });
3641 let base_text_snapshot = diff.read_with(cx, |diff, cx| diff.base_text(cx));
3642 let update = diff
3643 .update(cx, |diff, cx| {
3644 diff.update_diff(
3645 snapshot.clone(),
3646 &base_text_snapshot,
3647 Some(Arc::from(base_text_snapshot.text())),
3648 cx,
3649 )
3650 })
3651 .await;
3652 diff.update(cx, |diff, cx| diff.set_snapshot(update, cx));
3653 cx.run_until_parked();
3654 drop(subscription);
3655 let events = rx.into_iter().collect::<Vec<_>>();
3656 match events.as_slice() {
3657 [
3658 BufferDiffEvent::DiffChanged(DiffChanged {
3659 changed_range: _,
3660 base_text_changed_range,
3661 extended_range: _,
3662 base_text_changed: _,
3663 }),
3664 ] => {
3665 // TODO(cole) this seems like it should pass but currently fails (see compare_hunks)
3666 // assert_eq!(
3667 // *changed_range,
3668 // Some(Anchor::min_max_range_for_buffer(
3669 // buffer.read_with(cx, |buffer, _| buffer.remote_id())
3670 // ))
3671 // );
3672 assert_eq!(*base_text_changed_range, Some(0..base_text.len()));
3673 }
3674 _ => panic!("unexpected events: {:?}", events),
3675 }
3676 }
3677
3678 #[gpui::test]
3679 async fn test_extended_range(cx: &mut TestAppContext) {
3680 let base_text = "
3681 aaa
3682 bbb
3683
3684
3685
3686
3687
3688 ccc
3689 ddd
3690 "
3691 .unindent();
3692
3693 let buffer_text = "
3694 aaa
3695 bbb
3696
3697
3698
3699
3700
3701 CCC
3702 ddd
3703 "
3704 .unindent();
3705
3706 let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
3707 let old_buffer = buffer.snapshot().clone();
3708 let diff_a = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
3709
3710 buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")]);
3711 let diff_b = BufferDiffSnapshot::new_sync(&buffer, base_text, cx);
3712
3713 let DiffChanged {
3714 changed_range,
3715 base_text_changed_range: _,
3716 extended_range,
3717 base_text_changed: _,
3718 } = compare_hunks(
3719 &diff_b.hunks,
3720 &diff_a.hunks,
3721 &old_buffer,
3722 &buffer,
3723 &diff_a.base_text(),
3724 &diff_a.base_text(),
3725 );
3726
3727 let changed_range = changed_range.unwrap();
3728 assert_eq!(
3729 changed_range.to_point(&buffer),
3730 Point::new(7, 0)..Point::new(9, 0),
3731 "changed_range should span from old hunk position to new hunk end"
3732 );
3733
3734 let extended_range = extended_range.unwrap();
3735 assert_eq!(
3736 extended_range.start.to_point(&buffer),
3737 Point::new(1, 3),
3738 "extended_range.start should extend to include the edit outside changed_range"
3739 );
3740 assert_eq!(
3741 extended_range.end.to_point(&buffer),
3742 Point::new(9, 0),
3743 "extended_range.end should collapse to changed_range.end when no edits in end margin"
3744 );
3745
3746 let base_text_2 = "
3747 one
3748 two
3749 three
3750 four
3751 five
3752 six
3753 seven
3754 eight
3755 "
3756 .unindent();
3757
3758 let buffer_text_2 = "
3759 ONE
3760 two
3761 THREE
3762 four
3763 FIVE
3764 six
3765 SEVEN
3766 eight
3767 "
3768 .unindent();
3769
3770 let mut buffer_2 = Buffer::new(ReplicaId::LOCAL, BufferId::new(2).unwrap(), buffer_text_2);
3771 let old_buffer_2 = buffer_2.snapshot().clone();
3772 let diff_2a = BufferDiffSnapshot::new_sync(&buffer_2, base_text_2.clone(), cx);
3773
3774 buffer_2.edit([(Point::new(4, 0)..Point::new(4, 4), "FIVE_CHANGED")]);
3775 let diff_2b = BufferDiffSnapshot::new_sync(&buffer_2, base_text_2, cx);
3776
3777 let DiffChanged {
3778 changed_range,
3779 base_text_changed_range: _,
3780 extended_range,
3781 base_text_changed: _,
3782 } = compare_hunks(
3783 &diff_2b.hunks,
3784 &diff_2a.hunks,
3785 &old_buffer_2,
3786 &buffer_2,
3787 &diff_2a.base_text(),
3788 &diff_2a.base_text(),
3789 );
3790
3791 let changed_range = changed_range.unwrap();
3792 assert_eq!(
3793 changed_range.to_point(&buffer_2),
3794 Point::new(4, 0)..Point::new(5, 0),
3795 "changed_range should be just the hunk that changed (FIVE)"
3796 );
3797
3798 let extended_range = extended_range.unwrap();
3799 assert_eq!(
3800 extended_range.to_point(&buffer_2),
3801 Point::new(4, 0)..Point::new(5, 0),
3802 "extended_range should equal changed_range when edit is within the hunk"
3803 );
3804 }
3805
3806 #[gpui::test]
3807 async fn test_buffer_diff_compare_with_base_text_change(_cx: &mut TestAppContext) {
3808 // Use a shared base text buffer so that anchors from old and new snapshots
3809 // share the same remote_id and resolve correctly across versions.
3810 let initial_base = "aaa\nbbb\nccc\nddd\neee\n";
3811 let mut base_text_buffer = Buffer::new(
3812 ReplicaId::LOCAL,
3813 BufferId::new(99).unwrap(),
3814 initial_base.to_string(),
3815 );
3816
3817 // --- Scenario 1: Base text gains a line, producing a new deletion hunk ---
3818 //
3819 // Buffer has a modification (ccc → CCC). When the base text gains
3820 // a new line "XXX" after "aaa", the diff now also contains a
3821 // deletion for that line, and the modification hunk shifts in the
3822 // base text.
3823 let buffer_text_1 = "aaa\nbbb\nCCC\nddd\neee\n";
3824 let buffer = Buffer::new(
3825 ReplicaId::LOCAL,
3826 BufferId::new(1).unwrap(),
3827 buffer_text_1.to_string(),
3828 );
3829
3830 let old_base_snapshot_1 = base_text_buffer.snapshot().clone();
3831 let old_hunks_1 = compute_hunks(
3832 Some((Arc::from(initial_base), Rope::from(initial_base))),
3833 buffer.snapshot(),
3834 None,
3835 );
3836
3837 // Insert "XXX\n" after "aaa\n" in the base text.
3838 base_text_buffer.edit([(4..4, "XXX\n")]);
3839 let new_base_str_1: Arc<str> = Arc::from(base_text_buffer.text().as_str());
3840 let new_base_snapshot_1 = base_text_buffer.snapshot();
3841
3842 let new_hunks_1 = compute_hunks(
3843 Some((new_base_str_1.clone(), Rope::from(new_base_str_1.as_ref()))),
3844 buffer.snapshot(),
3845 None,
3846 );
3847
3848 let DiffChanged {
3849 changed_range,
3850 base_text_changed_range,
3851 extended_range: _,
3852 base_text_changed: _,
3853 } = compare_hunks(
3854 &new_hunks_1,
3855 &old_hunks_1,
3856 &buffer.snapshot(),
3857 &buffer.snapshot(),
3858 &old_base_snapshot_1,
3859 &new_base_snapshot_1,
3860 );
3861
3862 // The new deletion hunk (XXX) starts at buffer row 1 and the
3863 // modification hunk (ccc → CCC) now has a different
3864 // diff_base_byte_range, so the changed range spans both.
3865 let range = changed_range.unwrap();
3866 assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(3, 0),);
3867 let base_range = base_text_changed_range.unwrap();
3868 assert_eq!(
3869 base_range.to_point(&new_base_snapshot_1),
3870 Point::new(1, 0)..Point::new(4, 0),
3871 );
3872
3873 // --- Scenario 2: Base text changes to match the buffer (hunk disappears) ---
3874 //
3875 // Start fresh with a simple base text.
3876 let simple_base = "one\ntwo\nthree\n";
3877 let mut base_buf_2 = Buffer::new(
3878 ReplicaId::LOCAL,
3879 BufferId::new(100).unwrap(),
3880 simple_base.to_string(),
3881 );
3882
3883 let buffer_text_2 = "one\nTWO\nthree\n";
3884 let buffer_2 = Buffer::new(
3885 ReplicaId::LOCAL,
3886 BufferId::new(2).unwrap(),
3887 buffer_text_2.to_string(),
3888 );
3889
3890 let old_base_snapshot_2 = base_buf_2.snapshot().clone();
3891 let old_hunks_2 = compute_hunks(
3892 Some((Arc::from(simple_base), Rope::from(simple_base))),
3893 buffer_2.snapshot(),
3894 None,
3895 );
3896
3897 // The base text is edited so "two" becomes "TWO", now matching the buffer.
3898 base_buf_2.edit([(4..7, "TWO")]);
3899 let new_base_str_2: Arc<str> = Arc::from(base_buf_2.text().as_str());
3900 let new_base_snapshot_2 = base_buf_2.snapshot();
3901
3902 let new_hunks_2 = compute_hunks(
3903 Some((new_base_str_2.clone(), Rope::from(new_base_str_2.as_ref()))),
3904 buffer_2.snapshot(),
3905 None,
3906 );
3907
3908 let DiffChanged {
3909 changed_range,
3910 base_text_changed_range,
3911 extended_range: _,
3912 base_text_changed: _,
3913 } = compare_hunks(
3914 &new_hunks_2,
3915 &old_hunks_2,
3916 &buffer_2.snapshot(),
3917 &buffer_2.snapshot(),
3918 &old_base_snapshot_2,
3919 &new_base_snapshot_2,
3920 );
3921
3922 // The old modification hunk (two → TWO) is now gone because the
3923 // base text matches the buffer. The changed range covers where the
3924 // old hunk used to be.
3925 let range = changed_range.unwrap();
3926 assert_eq!(
3927 range.to_point(&buffer_2),
3928 Point::new(1, 0)..Point::new(2, 0),
3929 );
3930 let base_range = base_text_changed_range.unwrap();
3931 // The old hunk's diff_base_byte_range covered "two\n" (bytes 4..8).
3932 // anchor_after(4) is right-biased at the start of the deleted "two",
3933 // so after the edit replacing "two" with "TWO" it resolves past the
3934 // insertion to Point(1, 3).
3935 assert_eq!(
3936 base_range.to_point(&new_base_snapshot_2),
3937 Point::new(1, 3)..Point::new(2, 0),
3938 );
3939
3940 // --- Scenario 3: Base text edit changes one hunk but not another ---
3941 //
3942 // Two modification hunks exist. Only one of them is resolved by
3943 // the base text change; the other remains identical.
3944 let base_3 = "aaa\nbbb\nccc\nddd\neee\n";
3945 let mut base_buf_3 = Buffer::new(
3946 ReplicaId::LOCAL,
3947 BufferId::new(101).unwrap(),
3948 base_3.to_string(),
3949 );
3950
3951 let buffer_text_3 = "aaa\nBBB\nccc\nDDD\neee\n";
3952 let buffer_3 = Buffer::new(
3953 ReplicaId::LOCAL,
3954 BufferId::new(3).unwrap(),
3955 buffer_text_3.to_string(),
3956 );
3957
3958 let old_base_snapshot_3 = base_buf_3.snapshot().clone();
3959 let old_hunks_3 = compute_hunks(
3960 Some((Arc::from(base_3), Rope::from(base_3))),
3961 buffer_3.snapshot(),
3962 None,
3963 );
3964
3965 // Change "ddd" to "DDD" in the base text so that hunk disappears,
3966 // but "bbb" stays, so its hunk remains.
3967 base_buf_3.edit([(12..15, "DDD")]);
3968 let new_base_str_3: Arc<str> = Arc::from(base_buf_3.text().as_str());
3969 let new_base_snapshot_3 = base_buf_3.snapshot();
3970
3971 let new_hunks_3 = compute_hunks(
3972 Some((new_base_str_3.clone(), Rope::from(new_base_str_3.as_ref()))),
3973 buffer_3.snapshot(),
3974 None,
3975 );
3976
3977 let DiffChanged {
3978 changed_range,
3979 base_text_changed_range,
3980 extended_range: _,
3981 base_text_changed: _,
3982 } = compare_hunks(
3983 &new_hunks_3,
3984 &old_hunks_3,
3985 &buffer_3.snapshot(),
3986 &buffer_3.snapshot(),
3987 &old_base_snapshot_3,
3988 &new_base_snapshot_3,
3989 );
3990
3991 // Only the second hunk (ddd → DDD) disappeared; the first hunk
3992 // (bbb → BBB) is unchanged, so the changed range covers only line 3.
3993 let range = changed_range.unwrap();
3994 assert_eq!(
3995 range.to_point(&buffer_3),
3996 Point::new(3, 0)..Point::new(4, 0),
3997 );
3998 let base_range = base_text_changed_range.unwrap();
3999 // anchor_after(12) is right-biased at the start of deleted "ddd",
4000 // so after the edit replacing "ddd" with "DDD" it resolves past
4001 // the insertion to Point(3, 3).
4002 assert_eq!(
4003 base_range.to_point(&new_base_snapshot_3),
4004 Point::new(3, 3)..Point::new(4, 0),
4005 );
4006
4007 // --- Scenario 4: Both buffer and base text change simultaneously ---
4008 //
4009 // The buffer gains an edit that introduces a new hunk while the
4010 // base text also changes.
4011 let base_4 = "alpha\nbeta\ngamma\ndelta\n";
4012 let mut base_buf_4 = Buffer::new(
4013 ReplicaId::LOCAL,
4014 BufferId::new(102).unwrap(),
4015 base_4.to_string(),
4016 );
4017
4018 let buffer_text_4 = "alpha\nBETA\ngamma\ndelta\n";
4019 let mut buffer_4 = Buffer::new(
4020 ReplicaId::LOCAL,
4021 BufferId::new(4).unwrap(),
4022 buffer_text_4.to_string(),
4023 );
4024
4025 let old_base_snapshot_4 = base_buf_4.snapshot().clone();
4026 let old_buffer_snapshot_4 = buffer_4.snapshot().clone();
4027 let old_hunks_4 = compute_hunks(
4028 Some((Arc::from(base_4), Rope::from(base_4))),
4029 buffer_4.snapshot(),
4030 None,
4031 );
4032
4033 // Edit the buffer: change "delta" to "DELTA" (new modification hunk).
4034 buffer_4.edit_via_marked_text(
4035 &"
4036 alpha
4037 BETA
4038 gamma
4039 «DELTA»
4040 "
4041 .unindent(),
4042 );
4043
4044 // Edit the base text: change "beta" to "BETA" (resolves that hunk).
4045 base_buf_4.edit([(6..10, "BETA")]);
4046 let new_base_str_4: Arc<str> = Arc::from(base_buf_4.text().as_str());
4047 let new_base_snapshot_4 = base_buf_4.snapshot();
4048
4049 let new_hunks_4 = compute_hunks(
4050 Some((new_base_str_4.clone(), Rope::from(new_base_str_4.as_ref()))),
4051 buffer_4.snapshot(),
4052 None,
4053 );
4054
4055 let DiffChanged {
4056 changed_range,
4057 base_text_changed_range,
4058 extended_range: _,
4059 base_text_changed: _,
4060 } = compare_hunks(
4061 &new_hunks_4,
4062 &old_hunks_4,
4063 &old_buffer_snapshot_4,
4064 &buffer_4.snapshot(),
4065 &old_base_snapshot_4,
4066 &new_base_snapshot_4,
4067 );
4068
4069 // The old BETA hunk (line 1) is gone and a new DELTA hunk (line 3)
4070 // appeared, so the changed range spans from line 1 through line 4.
4071 let range = changed_range.unwrap();
4072 assert_eq!(
4073 range.to_point(&buffer_4),
4074 Point::new(1, 0)..Point::new(4, 0),
4075 );
4076 let base_range = base_text_changed_range.unwrap();
4077 // The old BETA hunk's base range started at byte 6 ("beta"). After
4078 // the base text edit replacing "beta" with "BETA", anchor_after(6)
4079 // resolves past the insertion to Point(1, 4).
4080 assert_eq!(
4081 base_range.to_point(&new_base_snapshot_4),
4082 Point::new(1, 4)..Point::new(4, 0),
4083 );
4084 }
4085
4086 #[gpui::test(iterations = 100)]
4087 async fn test_patch_for_range_random(cx: &mut TestAppContext, mut rng: StdRng) {
4088 fn gen_line(rng: &mut StdRng) -> String {
4089 if rng.random_bool(0.2) {
4090 "\n".to_owned()
4091 } else {
4092 let c = rng.random_range('A'..='Z');
4093 format!("{c}{c}{c}\n")
4094 }
4095 }
4096
4097 fn gen_text(rng: &mut StdRng, line_count: usize) -> String {
4098 (0..line_count).map(|_| gen_line(rng)).collect()
4099 }
4100
4101 fn gen_edits_from(rng: &mut StdRng, base: &str) -> String {
4102 let mut old_lines: Vec<&str> = base.lines().collect();
4103 let mut result = String::new();
4104
4105 while !old_lines.is_empty() {
4106 let unchanged_count = rng.random_range(0..=old_lines.len());
4107 for _ in 0..unchanged_count {
4108 if old_lines.is_empty() {
4109 break;
4110 }
4111 result.push_str(old_lines.remove(0));
4112 result.push('\n');
4113 }
4114
4115 if old_lines.is_empty() {
4116 break;
4117 }
4118
4119 let deleted_count = rng.random_range(0..=old_lines.len().min(3));
4120 for _ in 0..deleted_count {
4121 if old_lines.is_empty() {
4122 break;
4123 }
4124 old_lines.remove(0);
4125 }
4126
4127 let minimum_added = if deleted_count == 0 { 1 } else { 0 };
4128 let added_count = rng.random_range(minimum_added..=3);
4129 for _ in 0..added_count {
4130 result.push_str(&gen_line(rng));
4131 }
4132 }
4133
4134 result
4135 }
4136
4137 fn random_point_in_text(rng: &mut StdRng, lines: &[&str]) -> Point {
4138 if lines.is_empty() {
4139 return Point::zero();
4140 }
4141 let row = rng.random_range(0..lines.len() as u32);
4142 let line = lines[row as usize];
4143 let col = if line.is_empty() {
4144 0
4145 } else {
4146 rng.random_range(0..=line.len() as u32)
4147 };
4148 Point::new(row, col)
4149 }
4150
4151 fn random_range_in_text(rng: &mut StdRng, lines: &[&str]) -> RangeInclusive<Point> {
4152 let start = random_point_in_text(rng, lines);
4153 let end = random_point_in_text(rng, lines);
4154 if start <= end {
4155 start..=end
4156 } else {
4157 end..=start
4158 }
4159 }
4160
4161 fn points_in_range(range: &RangeInclusive<Point>, lines: &[&str]) -> Vec<Point> {
4162 let mut points = Vec::new();
4163 for row in range.start().row..=range.end().row {
4164 if row as usize >= lines.len() {
4165 points.push(Point::new(row, 0));
4166 continue;
4167 }
4168 let line = lines[row as usize];
4169 let start_col = if row == range.start().row {
4170 range.start().column
4171 } else {
4172 0
4173 };
4174 let end_col = if row == range.end().row {
4175 range.end().column
4176 } else {
4177 line.len() as u32
4178 };
4179 for col in start_col..=end_col {
4180 points.push(Point::new(row, col));
4181 }
4182 }
4183 points
4184 }
4185
4186 let rng = &mut rng;
4187
4188 let line_count = rng.random_range(5..20);
4189 let base_text = gen_text(rng, line_count);
4190 let initial_buffer_text = gen_edits_from(rng, &base_text);
4191
4192 let mut buffer = Buffer::new(
4193 ReplicaId::LOCAL,
4194 BufferId::new(1).unwrap(),
4195 initial_buffer_text.clone(),
4196 );
4197
4198 let diff = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
4199
4200 let edit_count = rng.random_range(1..=5);
4201 for _ in 0..edit_count {
4202 let buffer_text = buffer.text();
4203 if buffer_text.is_empty() {
4204 buffer.edit([(0..0, gen_line(rng))]);
4205 } else {
4206 let lines: Vec<&str> = buffer_text.lines().collect();
4207 let start_row = rng.random_range(0..lines.len());
4208 let end_row = rng.random_range(start_row..=lines.len().min(start_row + 3));
4209
4210 let start_col = if start_row < lines.len() {
4211 rng.random_range(0..=lines[start_row].len())
4212 } else {
4213 0
4214 };
4215 let end_col = if end_row < lines.len() {
4216 rng.random_range(0..=lines[end_row].len())
4217 } else {
4218 0
4219 };
4220
4221 let start_offset = buffer
4222 .point_to_offset(Point::new(start_row as u32, start_col as u32))
4223 .min(buffer.len());
4224 let end_offset = buffer
4225 .point_to_offset(Point::new(end_row as u32, end_col as u32))
4226 .min(buffer.len());
4227
4228 let (start, end) = if start_offset <= end_offset {
4229 (start_offset, end_offset)
4230 } else {
4231 (end_offset, start_offset)
4232 };
4233
4234 let new_text = if rng.random_bool(0.3) {
4235 String::new()
4236 } else {
4237 let line_count = rng.random_range(0..=2);
4238 gen_text(rng, line_count)
4239 };
4240
4241 buffer.edit([(start..end, new_text)]);
4242 }
4243 }
4244
4245 let buffer_snapshot = buffer.snapshot();
4246
4247 let buffer_text = buffer_snapshot.text();
4248 let buffer_lines: Vec<&str> = buffer_text.lines().collect();
4249 let base_lines: Vec<&str> = base_text.lines().collect();
4250
4251 let test_count = 10;
4252 for _ in 0..test_count {
4253 let range = random_range_in_text(rng, &buffer_lines);
4254 let points = points_in_range(&range, &buffer_lines);
4255
4256 let optimized_patch = diff.patch_for_buffer_range(range.clone(), &buffer_snapshot);
4257 let naive_patch = diff.patch_for_buffer_range_naive(&buffer_snapshot);
4258
4259 for point in points {
4260 let optimized_edit = optimized_patch.edit_for_old_position(point);
4261 let naive_edit = naive_patch.edit_for_old_position(point);
4262
4263 assert_eq!(
4264 optimized_edit,
4265 naive_edit,
4266 "patch_for_buffer_range mismatch at point {:?} in range {:?}\nbase_text: {:?}\ninitial_buffer: {:?}\ncurrent_buffer: {:?}",
4267 point,
4268 range,
4269 base_text,
4270 initial_buffer_text,
4271 buffer_snapshot.text()
4272 );
4273 }
4274 }
4275
4276 for _ in 0..test_count {
4277 let range = random_range_in_text(rng, &base_lines);
4278 let points = points_in_range(&range, &base_lines);
4279
4280 let optimized_patch = diff.patch_for_base_text_range(range.clone(), &buffer_snapshot);
4281 let naive_patch = diff.patch_for_base_text_range_naive(&buffer_snapshot);
4282
4283 for point in points {
4284 let optimized_edit = optimized_patch.edit_for_old_position(point);
4285 let naive_edit = naive_patch.edit_for_old_position(point);
4286
4287 assert_eq!(
4288 optimized_edit,
4289 naive_edit,
4290 "patch_for_base_text_range mismatch at point {:?} in range {:?}\nbase_text: {:?}\ninitial_buffer: {:?}\ncurrent_buffer: {:?}",
4291 point,
4292 range,
4293 base_text,
4294 initial_buffer_text,
4295 buffer_snapshot.text()
4296 );
4297 }
4298 }
4299 }
4300
4301 #[gpui::test]
4302 async fn test_set_base_text_with_crlf(cx: &mut gpui::TestAppContext) {
4303 let base_text_crlf = "one\r\ntwo\r\nthree\r\nfour\r\nfive\r\n";
4304 let base_text_lf = "one\ntwo\nthree\nfour\nfive\n";
4305 assert_ne!(base_text_crlf.len(), base_text_lf.len());
4306
4307 let buffer_text = "one\nTWO\nthree\nfour\nfive\n";
4308 let buffer = Buffer::new(
4309 ReplicaId::LOCAL,
4310 BufferId::new(1).unwrap(),
4311 buffer_text.to_string(),
4312 );
4313 let buffer_snapshot = buffer.snapshot();
4314
4315 let diff = cx.new(|cx| BufferDiff::new(&buffer_snapshot, None, None, cx));
4316 diff.update(cx, |diff, cx| {
4317 diff.set_base_text(Some(Arc::from(base_text_crlf)), buffer_snapshot.clone(), cx)
4318 })
4319 .await;
4320 cx.run_until_parked();
4321
4322 let snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx));
4323 snapshot.buffer_point_to_base_text_range(Point::new(0, 0), &buffer_snapshot);
4324 snapshot.buffer_point_to_base_text_range(Point::new(1, 0), &buffer_snapshot);
4325 }
4326}
4327