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}