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