conflict_set.rs

  1use gpui::{App, Context, Entity, EventEmitter, SharedString};
  2use std::{cmp::Ordering, ops::Range, sync::Arc};
  3use text::{Anchor, BufferId, OffsetRangeExt as _};
  4
  5pub struct ConflictSet {
  6    pub has_conflict: bool,
  7    pub snapshot: ConflictSetSnapshot,
  8}
  9
 10#[derive(Clone, Debug, PartialEq, Eq)]
 11pub struct ConflictSetUpdate {
 12    pub buffer_range: Option<Range<Anchor>>,
 13    pub old_range: Range<usize>,
 14    pub new_range: Range<usize>,
 15}
 16
 17#[derive(Debug, Clone)]
 18pub struct ConflictSetSnapshot {
 19    pub buffer_id: BufferId,
 20    pub conflicts: Arc<[ConflictRegion]>,
 21}
 22
 23impl ConflictSetSnapshot {
 24    pub fn conflicts_in_range(
 25        &self,
 26        range: Range<Anchor>,
 27        buffer: &text::BufferSnapshot,
 28    ) -> &[ConflictRegion] {
 29        let start_ix = self
 30            .conflicts
 31            .binary_search_by(|conflict| {
 32                conflict
 33                    .range
 34                    .end
 35                    .cmp(&range.start, buffer)
 36                    .then(Ordering::Greater)
 37            })
 38            .unwrap_err();
 39        let end_ix = start_ix
 40            + self.conflicts[start_ix..]
 41                .binary_search_by(|conflict| {
 42                    conflict
 43                        .range
 44                        .start
 45                        .cmp(&range.end, buffer)
 46                        .then(Ordering::Less)
 47                })
 48                .unwrap_err();
 49        &self.conflicts[start_ix..end_ix]
 50    }
 51
 52    pub fn compare(&self, other: &Self, buffer: &text::BufferSnapshot) -> ConflictSetUpdate {
 53        let common_prefix_len = self
 54            .conflicts
 55            .iter()
 56            .zip(other.conflicts.iter())
 57            .take_while(|(old, new)| old == new)
 58            .count();
 59        let common_suffix_len = self.conflicts[common_prefix_len..]
 60            .iter()
 61            .rev()
 62            .zip(other.conflicts[common_prefix_len..].iter().rev())
 63            .take_while(|(old, new)| old == new)
 64            .count();
 65        let old_conflicts =
 66            &self.conflicts[common_prefix_len..(self.conflicts.len() - common_suffix_len)];
 67        let new_conflicts =
 68            &other.conflicts[common_prefix_len..(other.conflicts.len() - common_suffix_len)];
 69        let old_range = common_prefix_len..(common_prefix_len + old_conflicts.len());
 70        let new_range = common_prefix_len..(common_prefix_len + new_conflicts.len());
 71        let start = match (old_conflicts.first(), new_conflicts.first()) {
 72            (None, None) => None,
 73            (None, Some(conflict)) => Some(conflict.range.start),
 74            (Some(conflict), None) => Some(conflict.range.start),
 75            (Some(first), Some(second)) => {
 76                Some(*first.range.start.min(&second.range.start, buffer))
 77            }
 78        };
 79        let end = match (old_conflicts.last(), new_conflicts.last()) {
 80            (None, None) => None,
 81            (None, Some(conflict)) => Some(conflict.range.end),
 82            (Some(first), None) => Some(first.range.end),
 83            (Some(first), Some(second)) => Some(*first.range.end.max(&second.range.end, buffer)),
 84        };
 85        ConflictSetUpdate {
 86            buffer_range: start.zip(end).map(|(start, end)| start..end),
 87            old_range,
 88            new_range,
 89        }
 90    }
 91}
 92
 93#[derive(Debug, Clone, PartialEq, Eq)]
 94pub struct ConflictRegion {
 95    pub ours_branch_name: SharedString,
 96    pub theirs_branch_name: SharedString,
 97    pub range: Range<Anchor>,
 98    pub ours: Range<Anchor>,
 99    pub theirs: Range<Anchor>,
100    pub base: Option<Range<Anchor>>,
101}
102
103impl ConflictRegion {
104    pub fn resolve(
105        &self,
106        buffer: Entity<language::Buffer>,
107        ranges: &[Range<Anchor>],
108        cx: &mut App,
109    ) {
110        let buffer_snapshot = buffer.read(cx).snapshot();
111        let mut deletions = Vec::new();
112        let empty = "";
113        let outer_range = self.range.to_offset(&buffer_snapshot);
114        let mut offset = outer_range.start;
115        for kept_range in ranges {
116            let kept_range = kept_range.to_offset(&buffer_snapshot);
117            if kept_range.start > offset {
118                deletions.push((offset..kept_range.start, empty));
119            }
120            offset = kept_range.end;
121        }
122        if outer_range.end > offset {
123            deletions.push((offset..outer_range.end, empty));
124        }
125
126        buffer.update(cx, |buffer, cx| {
127            buffer.edit(deletions, None, cx);
128        });
129    }
130}
131
132impl ConflictSet {
133    pub fn new(buffer_id: BufferId, has_conflict: bool, _: &mut Context<Self>) -> Self {
134        Self {
135            has_conflict,
136            snapshot: ConflictSetSnapshot {
137                buffer_id,
138                conflicts: Default::default(),
139            },
140        }
141    }
142
143    pub fn set_has_conflict(&mut self, has_conflict: bool, cx: &mut Context<Self>) -> bool {
144        if has_conflict != self.has_conflict {
145            self.has_conflict = has_conflict;
146            if !self.has_conflict {
147                cx.emit(ConflictSetUpdate {
148                    buffer_range: None,
149                    old_range: 0..self.snapshot.conflicts.len(),
150                    new_range: 0..0,
151                });
152                self.snapshot.conflicts = Default::default();
153            }
154            true
155        } else {
156            false
157        }
158    }
159
160    pub fn snapshot(&self) -> ConflictSetSnapshot {
161        self.snapshot.clone()
162    }
163
164    pub fn set_snapshot(
165        &mut self,
166        snapshot: ConflictSetSnapshot,
167        update: ConflictSetUpdate,
168        cx: &mut Context<Self>,
169    ) {
170        self.snapshot = snapshot;
171        cx.emit(update);
172    }
173
174    pub fn parse(buffer: &text::BufferSnapshot) -> ConflictSetSnapshot {
175        let mut conflicts = Vec::new();
176
177        let mut line_pos = 0;
178        let buffer_len = buffer.len();
179        let mut lines = buffer.text_for_range(0..buffer_len).lines();
180
181        let mut conflict_start: Option<usize> = None;
182        let mut ours_start: Option<usize> = None;
183        let mut ours_end: Option<usize> = None;
184        let mut ours_branch_name: Option<SharedString> = None;
185        let mut base_start: Option<usize> = None;
186        let mut base_end: Option<usize> = None;
187        let mut theirs_start: Option<usize> = None;
188        let mut theirs_branch_name: Option<SharedString> = None;
189
190        while let Some(line) = lines.next() {
191            let line_end = line_pos + line.len();
192
193            if let Some(branch_name) = line.strip_prefix("<<<<<<< ") {
194                // If we see a new conflict marker while already parsing one,
195                // abandon the previous one and start a new one
196                conflict_start = Some(line_pos);
197                ours_start = Some(line_end + 1);
198
199                let branch_name = branch_name.trim();
200                if !branch_name.is_empty() {
201                    ours_branch_name = Some(SharedString::new(branch_name));
202                }
203            } else if line.starts_with("||||||| ")
204                && conflict_start.is_some()
205                && ours_start.is_some()
206            {
207                ours_end = Some(line_pos);
208                base_start = Some(line_end + 1);
209            } else if line.starts_with("=======")
210                && conflict_start.is_some()
211                && ours_start.is_some()
212            {
213                // Set ours_end if not already set (would be set if we have base markers)
214                if ours_end.is_none() {
215                    ours_end = Some(line_pos);
216                } else if base_start.is_some() {
217                    base_end = Some(line_pos);
218                }
219                theirs_start = Some(line_end + 1);
220            } else if let Some(branch_name) = line.strip_prefix(">>>>>>> ")
221                && conflict_start.is_some()
222                && ours_start.is_some()
223                && ours_end.is_some()
224                && theirs_start.is_some()
225            {
226                let branch_name = branch_name.trim();
227                if !branch_name.is_empty() {
228                    theirs_branch_name = Some(SharedString::new(branch_name));
229                }
230
231                let theirs_end = line_pos;
232                let conflict_end = (line_end + 1).min(buffer_len);
233
234                let range = buffer.anchor_after(conflict_start.unwrap())
235                    ..buffer.anchor_before(conflict_end);
236                let ours = buffer.anchor_after(ours_start.unwrap())
237                    ..buffer.anchor_before(ours_end.unwrap());
238                let theirs =
239                    buffer.anchor_after(theirs_start.unwrap())..buffer.anchor_before(theirs_end);
240
241                let base = base_start
242                    .zip(base_end)
243                    .map(|(start, end)| buffer.anchor_after(start)..buffer.anchor_before(end));
244
245                conflicts.push(ConflictRegion {
246                    ours_branch_name: ours_branch_name
247                        .take()
248                        .unwrap_or_else(|| SharedString::new_static("HEAD")),
249                    theirs_branch_name: theirs_branch_name
250                        .take()
251                        .unwrap_or_else(|| SharedString::new_static("Origin")),
252                    range,
253                    ours,
254                    theirs,
255                    base,
256                });
257
258                conflict_start = None;
259                ours_start = None;
260                ours_end = None;
261                base_start = None;
262                base_end = None;
263                theirs_start = None;
264            }
265
266            line_pos = line_end + 1;
267        }
268
269        ConflictSetSnapshot {
270            conflicts: conflicts.into(),
271            buffer_id: buffer.remote_id(),
272        }
273    }
274}
275
276impl EventEmitter<ConflictSetUpdate> for ConflictSet {}
277
278#[cfg(test)]
279mod tests {
280    use std::sync::mpsc;
281
282    use crate::Project;
283
284    use super::*;
285    use fs::FakeFs;
286    use git::{
287        repository::{RepoPath, repo_path},
288        status::{UnmergedStatus, UnmergedStatusCode},
289    };
290    use gpui::{BackgroundExecutor, TestAppContext};
291    use serde_json::json;
292    use text::{Buffer, BufferId, Point, ReplicaId, ToOffset as _};
293    use unindent::Unindent as _;
294    use util::{path, rel_path::rel_path};
295
296    #[test]
297    fn test_parse_conflicts_in_buffer() {
298        // Create a buffer with conflict markers
299        let test_content = r#"
300            This is some text before the conflict.
301            <<<<<<< HEAD
302            This is our version
303            =======
304            This is their version
305            >>>>>>> branch-name
306
307            Another conflict:
308            <<<<<<< HEAD
309            Our second change
310            ||||||| merged common ancestors
311            Original content
312            =======
313            Their second change
314            >>>>>>> branch-name
315        "#
316        .unindent();
317
318        let buffer_id = BufferId::new(1).unwrap();
319        let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content);
320        let snapshot = buffer.snapshot();
321
322        let conflict_snapshot = ConflictSet::parse(&snapshot);
323        assert_eq!(conflict_snapshot.conflicts.len(), 2);
324
325        let first = &conflict_snapshot.conflicts[0];
326        assert!(first.base.is_none());
327        assert_eq!(first.ours_branch_name.as_ref(), "HEAD");
328        assert_eq!(first.theirs_branch_name.as_ref(), "branch-name");
329        let our_text = snapshot
330            .text_for_range(first.ours.clone())
331            .collect::<String>();
332        let their_text = snapshot
333            .text_for_range(first.theirs.clone())
334            .collect::<String>();
335        assert_eq!(our_text, "This is our version\n");
336        assert_eq!(their_text, "This is their version\n");
337
338        let second = &conflict_snapshot.conflicts[1];
339        assert!(second.base.is_some());
340        assert_eq!(second.ours_branch_name.as_ref(), "HEAD");
341        assert_eq!(second.theirs_branch_name.as_ref(), "branch-name");
342        let our_text = snapshot
343            .text_for_range(second.ours.clone())
344            .collect::<String>();
345        let their_text = snapshot
346            .text_for_range(second.theirs.clone())
347            .collect::<String>();
348        let base_text = snapshot
349            .text_for_range(second.base.as_ref().unwrap().clone())
350            .collect::<String>();
351        assert_eq!(our_text, "Our second change\n");
352        assert_eq!(their_text, "Their second change\n");
353        assert_eq!(base_text, "Original content\n");
354
355        // Test conflicts_in_range
356        let range = snapshot.anchor_before(0)..snapshot.anchor_before(snapshot.len());
357        let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
358        assert_eq!(conflicts_in_range.len(), 2);
359
360        // Test with a range that includes only the first conflict
361        let first_conflict_end = conflict_snapshot.conflicts[0].range.end;
362        let range = snapshot.anchor_before(0)..first_conflict_end;
363        let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
364        assert_eq!(conflicts_in_range.len(), 1);
365
366        // Test with a range that includes only the second conflict
367        let second_conflict_start = conflict_snapshot.conflicts[1].range.start;
368        let range = second_conflict_start..snapshot.anchor_before(snapshot.len());
369        let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
370        assert_eq!(conflicts_in_range.len(), 1);
371
372        // Test with a range that doesn't include any conflicts
373        let range = buffer.anchor_after(first_conflict_end.to_next_offset(&buffer))
374            ..buffer.anchor_before(second_conflict_start.to_previous_offset(&buffer));
375        let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
376        assert_eq!(conflicts_in_range.len(), 0);
377    }
378
379    #[test]
380    fn test_nested_conflict_markers() {
381        // Create a buffer with nested conflict markers
382        let test_content = r#"
383            This is some text before the conflict.
384            <<<<<<< HEAD
385            This is our version
386            <<<<<<< HEAD
387            This is a nested conflict marker
388            =======
389            This is their version in a nested conflict
390            >>>>>>> branch-nested
391            =======
392            This is their version
393            >>>>>>> branch-name
394        "#
395        .unindent();
396
397        let buffer_id = BufferId::new(1).unwrap();
398        let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content);
399        let snapshot = buffer.snapshot();
400
401        let conflict_snapshot = ConflictSet::parse(&snapshot);
402
403        assert_eq!(conflict_snapshot.conflicts.len(), 1);
404
405        // The conflict should have our version, their version, but no base
406        let conflict = &conflict_snapshot.conflicts[0];
407        assert!(conflict.base.is_none());
408        assert_eq!(conflict.ours_branch_name.as_ref(), "HEAD");
409        assert_eq!(conflict.theirs_branch_name.as_ref(), "branch-nested");
410
411        // Check that the nested conflict was detected correctly
412        let our_text = snapshot
413            .text_for_range(conflict.ours.clone())
414            .collect::<String>();
415        assert_eq!(our_text, "This is a nested conflict marker\n");
416        let their_text = snapshot
417            .text_for_range(conflict.theirs.clone())
418            .collect::<String>();
419        assert_eq!(their_text, "This is their version in a nested conflict\n");
420    }
421
422    #[test]
423    fn test_conflict_markers_at_eof() {
424        let test_content = r#"
425            <<<<<<< ours
426            =======
427            This is their version
428            >>>>>>> "#
429            .unindent();
430        let buffer_id = BufferId::new(1).unwrap();
431        let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content);
432        let snapshot = buffer.snapshot();
433
434        let conflict_snapshot = ConflictSet::parse(&snapshot);
435        assert_eq!(conflict_snapshot.conflicts.len(), 1);
436        assert_eq!(
437            conflict_snapshot.conflicts[0].ours_branch_name.as_ref(),
438            "ours"
439        );
440        assert_eq!(
441            conflict_snapshot.conflicts[0].theirs_branch_name.as_ref(),
442            "Origin" // default branch name if there is none
443        );
444    }
445
446    #[test]
447    fn test_conflicts_in_range() {
448        // Create a buffer with conflict markers
449        let test_content = r#"
450            one
451            <<<<<<< HEAD1
452            two
453            =======
454            three
455            >>>>>>> branch1
456            four
457            five
458            <<<<<<< HEAD2
459            six
460            =======
461            seven
462            >>>>>>> branch2
463            eight
464            nine
465            <<<<<<< HEAD3
466            ten
467            =======
468            eleven
469            >>>>>>> branch3
470            twelve
471            <<<<<<< HEAD4
472            thirteen
473            =======
474            fourteen
475            >>>>>>> branch4
476            fifteen
477        "#
478        .unindent();
479
480        let buffer_id = BufferId::new(1).unwrap();
481        let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content.clone());
482        let snapshot = buffer.snapshot();
483
484        let conflict_snapshot = ConflictSet::parse(&snapshot);
485        assert_eq!(conflict_snapshot.conflicts.len(), 4);
486        assert_eq!(
487            conflict_snapshot.conflicts[0].ours_branch_name.as_ref(),
488            "HEAD1"
489        );
490        assert_eq!(
491            conflict_snapshot.conflicts[0].theirs_branch_name.as_ref(),
492            "branch1"
493        );
494        assert_eq!(
495            conflict_snapshot.conflicts[1].ours_branch_name.as_ref(),
496            "HEAD2"
497        );
498        assert_eq!(
499            conflict_snapshot.conflicts[1].theirs_branch_name.as_ref(),
500            "branch2"
501        );
502        assert_eq!(
503            conflict_snapshot.conflicts[2].ours_branch_name.as_ref(),
504            "HEAD3"
505        );
506        assert_eq!(
507            conflict_snapshot.conflicts[2].theirs_branch_name.as_ref(),
508            "branch3"
509        );
510        assert_eq!(
511            conflict_snapshot.conflicts[3].ours_branch_name.as_ref(),
512            "HEAD4"
513        );
514        assert_eq!(
515            conflict_snapshot.conflicts[3].theirs_branch_name.as_ref(),
516            "branch4"
517        );
518
519        let range = test_content.find("seven").unwrap()..test_content.find("eleven").unwrap();
520        let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
521        assert_eq!(
522            conflict_snapshot.conflicts_in_range(range, &snapshot),
523            &conflict_snapshot.conflicts[1..=2]
524        );
525
526        let range = test_content.find("one").unwrap()..test_content.find("<<<<<<< HEAD2").unwrap();
527        let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
528        assert_eq!(
529            conflict_snapshot.conflicts_in_range(range, &snapshot),
530            &conflict_snapshot.conflicts[0..=1]
531        );
532
533        let range =
534            test_content.find("eight").unwrap() - 1..test_content.find(">>>>>>> branch3").unwrap();
535        let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
536        assert_eq!(
537            conflict_snapshot.conflicts_in_range(range, &snapshot),
538            &conflict_snapshot.conflicts[1..=2]
539        );
540
541        let range = test_content.find("thirteen").unwrap() - 1..test_content.len();
542        let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
543        assert_eq!(
544            conflict_snapshot.conflicts_in_range(range, &snapshot),
545            &conflict_snapshot.conflicts[3..=3]
546        );
547    }
548
549    #[gpui::test]
550    async fn test_conflict_updates(executor: BackgroundExecutor, cx: &mut TestAppContext) {
551        zlog::init_test();
552        cx.update(|cx| {
553            settings::init(cx);
554        });
555        let initial_text = "
556            one
557            two
558            three
559            four
560            five
561        "
562        .unindent();
563        let fs = FakeFs::new(executor);
564        fs.insert_tree(
565            path!("/project"),
566            json!({
567                ".git": {},
568                "a.txt": initial_text,
569            }),
570        )
571        .await;
572        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
573        let (git_store, buffer) = project.update(cx, |project, cx| {
574            (
575                project.git_store().clone(),
576                project.open_local_buffer(path!("/project/a.txt"), cx),
577            )
578        });
579        let buffer = buffer.await.unwrap();
580        let conflict_set = git_store.update(cx, |git_store, cx| {
581            git_store.open_conflict_set(buffer.clone(), cx)
582        });
583        let (events_tx, events_rx) = mpsc::channel::<ConflictSetUpdate>();
584        let _conflict_set_subscription = cx.update(|cx| {
585            cx.subscribe(&conflict_set, move |_, event, _| {
586                events_tx.send(event.clone()).ok();
587            })
588        });
589        let conflicts_snapshot =
590            conflict_set.read_with(cx, |conflict_set, _| conflict_set.snapshot());
591        assert!(conflicts_snapshot.conflicts.is_empty());
592
593        buffer.update(cx, |buffer, cx| {
594            buffer.edit(
595                [
596                    (4..4, "<<<<<<< HEAD\n"),
597                    (14..14, "=======\nTWO\n>>>>>>> branch\n"),
598                ],
599                None,
600                cx,
601            );
602        });
603
604        cx.run_until_parked();
605        events_rx.try_recv().expect_err(
606            "no conflicts should be registered as long as the file's status is unchanged",
607        );
608
609        fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
610            state.unmerged_paths.insert(
611                repo_path("a.txt"),
612                UnmergedStatus {
613                    first_head: UnmergedStatusCode::Updated,
614                    second_head: UnmergedStatusCode::Updated,
615                },
616            );
617            // Cause the repository to emit MergeHeadsChanged.
618            state.refs.insert("MERGE_HEAD".into(), "123".into())
619        })
620        .unwrap();
621
622        cx.run_until_parked();
623        let update = events_rx
624            .try_recv()
625            .expect("status change should trigger conflict parsing");
626        assert_eq!(update.old_range, 0..0);
627        assert_eq!(update.new_range, 0..1);
628
629        let conflict = conflict_set.read_with(cx, |conflict_set, _| {
630            conflict_set.snapshot().conflicts[0].clone()
631        });
632        cx.update(|cx| {
633            conflict.resolve(buffer.clone(), std::slice::from_ref(&conflict.theirs), cx);
634        });
635
636        cx.run_until_parked();
637        let update = events_rx
638            .try_recv()
639            .expect("conflicts should be removed after resolution");
640        assert_eq!(update.old_range, 0..1);
641        assert_eq!(update.new_range, 0..0);
642    }
643
644    #[gpui::test]
645    async fn test_conflict_updates_without_merge_head(
646        executor: BackgroundExecutor,
647        cx: &mut TestAppContext,
648    ) {
649        zlog::init_test();
650        cx.update(|cx| {
651            settings::init(cx);
652        });
653
654        let initial_text = "
655            zero
656            <<<<<<< HEAD
657            one
658            =======
659            two
660            >>>>>>> Stashed Changes
661            three
662        "
663        .unindent();
664
665        let fs = FakeFs::new(executor);
666        fs.insert_tree(
667            path!("/project"),
668            json!({
669                ".git": {},
670                "a.txt": initial_text,
671            }),
672        )
673        .await;
674
675        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
676        let (git_store, buffer) = project.update(cx, |project, cx| {
677            (
678                project.git_store().clone(),
679                project.open_local_buffer(path!("/project/a.txt"), cx),
680            )
681        });
682
683        cx.run_until_parked();
684        fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
685            state.unmerged_paths.insert(
686                RepoPath::from_rel_path(rel_path("a.txt")),
687                UnmergedStatus {
688                    first_head: UnmergedStatusCode::Updated,
689                    second_head: UnmergedStatusCode::Updated,
690                },
691            )
692        })
693        .unwrap();
694
695        let buffer = buffer.await.unwrap();
696
697        // Open the conflict set for a file that currently has conflicts.
698        let conflict_set = git_store.update(cx, |git_store, cx| {
699            git_store.open_conflict_set(buffer.clone(), cx)
700        });
701
702        cx.run_until_parked();
703        conflict_set.update(cx, |conflict_set, cx| {
704            let conflict_range = conflict_set.snapshot().conflicts[0]
705                .range
706                .to_point(buffer.read(cx));
707            assert_eq!(conflict_range, Point::new(1, 0)..Point::new(6, 0));
708        });
709
710        // Simulate the conflict being removed by e.g. staging the file.
711        fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
712            state.unmerged_paths.remove(&repo_path("a.txt"))
713        })
714        .unwrap();
715
716        cx.run_until_parked();
717        conflict_set.update(cx, |conflict_set, _| {
718            assert!(!conflict_set.has_conflict);
719            assert_eq!(conflict_set.snapshot.conflicts.len(), 0);
720        });
721
722        // Simulate the conflict being re-added.
723        fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
724            state.unmerged_paths.insert(
725                repo_path("a.txt"),
726                UnmergedStatus {
727                    first_head: UnmergedStatusCode::Updated,
728                    second_head: UnmergedStatusCode::Updated,
729                },
730            )
731        })
732        .unwrap();
733
734        cx.run_until_parked();
735        conflict_set.update(cx, |conflict_set, cx| {
736            let conflict_range = conflict_set.snapshot().conflicts[0]
737                .range
738                .to_point(buffer.read(cx));
739            assert_eq!(conflict_range, Point::new(1, 0)..Point::new(6, 0));
740        });
741    }
742}