1use std::fmt;
30use std::ops::Range;
31
32use rich::cells::cell_len;
33use rich::measure::Measurement;
34use rich::{Console, ConsoleOptions, Renderable, Segment, Style, Text};
35
36use super::render::{banner, join, pad_to, trim_end, wrap_text};
37use super::source::{highlight_lines, language_for_path};
38use super::style;
39
40pub const MAX_CONFLICT_SOURCE: usize = 16 * 1024 * 1024;
42
43pub const MAX_CONFLICTS: usize = 10_000;
45
46const MARKER_SIZE: usize = 7;
48
49const SIDE_BY_SIDE_MIN: usize = 20;
52
53const HIGHLIGHT_LIMIT: usize = 1024 * 1024;
57
58#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct ConflictError {
61 pub line: Option<usize>,
64 pub message: String,
65}
66
67impl ConflictError {
68 fn at(line: usize, message: impl Into<String>) -> Self {
69 ConflictError {
70 line: Some(line),
71 message: message.into(),
72 }
73 }
74}
75
76impl fmt::Display for ConflictError {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self.line {
79 Some(line) => write!(f, "line {line}: {}", self.message),
80 None => f.write_str(&self.message),
81 }
82 }
83}
84
85impl std::error::Error for ConflictError {}
86
87#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct ConflictSide {
90 pub label: Option<String>,
92 pub marker_line: usize,
94 pub lines: Range<usize>,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct Conflict {
101 pub number: usize,
103 pub ours: ConflictSide,
105 pub base: Option<ConflictSide>,
107 pub theirs: ConflictSide,
110 pub end_line: usize,
112}
113
114impl Conflict {
115 pub fn start_line(&self) -> usize {
117 self.ours.marker_line
118 }
119
120 pub fn span(&self) -> Range<usize> {
122 self.start_line() - 1..self.end_line
123 }
124
125 pub fn sides(&self) -> Vec<(Pick, &ConflictSide)> {
127 let mut sides = vec![(Pick::Ours, &self.ours)];
128 if let Some(base) = &self.base {
129 sides.push((Pick::Base, base));
130 }
131 sides.push((Pick::Theirs, &self.theirs));
132 sides
133 }
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
138pub enum Pick {
139 Ours,
140 Base,
141 Theirs,
142}
143
144impl Pick {
145 pub fn name(self) -> &'static str {
147 match self {
148 Pick::Ours => "ours",
149 Pick::Base => "base",
150 Pick::Theirs => "theirs",
151 }
152 }
153
154 fn marker(self) -> char {
155 match self {
156 Pick::Ours => '<',
157 Pick::Base => '|',
158 Pick::Theirs => '>',
159 }
160 }
161
162 fn style_key(self) -> &'static str {
163 match self {
164 Pick::Ours => "diff.conflict.ours",
165 Pick::Base => "diff.conflict.base",
166 Pick::Theirs => "diff.conflict.theirs",
167 }
168 }
169}
170
171#[derive(Clone, Debug, PartialEq, Eq)]
184pub struct ConflictFile {
185 lines: Vec<String>,
186 conflicts: Vec<Conflict>,
187 crlf: bool,
188}
189
190fn marker(line: &str) -> Option<(char, usize, Option<&str>)> {
196 let first = line.chars().next()?;
197 if !matches!(first, '<' | '|' | '=' | '>') {
198 return None;
199 }
200 let length = line.bytes().take_while(|&b| b == first as u8).count();
201 if length < MARKER_SIZE {
202 return None;
203 }
204 let rest = &line[length..];
205 match rest.chars().next() {
206 None => Some((first, length, None)),
207 Some(c) if c.is_whitespace() => {
208 let label = rest.trim();
209 match (first, label.is_empty()) {
210 (_, true) => Some((first, length, None)),
211 ('=', false) => None,
213 (_, false) => Some((first, length, Some(label))),
214 }
215 }
216 Some(_) => None,
217 }
218}
219
220type LongerMarkers = std::collections::HashMap<usize, Vec<(usize, char)>>;
223
224fn closes(longer: &LongerMarkers, open: usize, length: usize) -> bool {
230 let Some(markers) = longer.get(&length) else {
231 return false;
232 };
233 let after = markers.partition_point(|&(line, _)| line <= open);
234 let mut separated = false;
235 for &(_, kind) in &markers[after..] {
236 match kind {
237 '<' => return false,
238 '=' => separated = true,
239 '>' if separated => return true,
240 _ => {}
241 }
242 }
243 false
244}
245
246enum State {
247 Outside,
248 Ours {
249 start: usize,
250 label: Option<String>,
251 },
252 Base {
253 ours: ConflictSide,
254 start: usize,
255 label: Option<String>,
256 },
257 Theirs {
258 ours: ConflictSide,
259 base: Option<ConflictSide>,
260 start: usize,
261 },
262}
263
264impl ConflictFile {
265 pub fn parse(text: &str) -> Result<ConflictFile, ConflictError> {
270 if text.len() > MAX_CONFLICT_SOURCE {
271 return Err(ConflictError {
272 line: None,
273 message: format!(
274 "the input is {} bytes; at most {MAX_CONFLICT_SOURCE} are read",
275 text.len()
276 ),
277 });
278 }
279 let text = text.strip_prefix('\u{feff}').unwrap_or(text);
280 let mut crlf = false;
281 let mut lines: Vec<String> = text
282 .split('\n')
283 .map(|line| match line.strip_suffix('\r') {
284 Some(line) => {
285 crlf = true;
286 line.to_string()
287 }
288 None => line.to_string(),
289 })
290 .collect();
291 if text.is_empty() || text.ends_with('\n') {
292 lines.pop();
293 }
294
295 let markers: Vec<Option<(char, usize, Option<&str>)>> =
298 lines.iter().map(|line| marker(line)).collect();
299 let mut longer = LongerMarkers::new();
300 for (index, found) in markers.iter().enumerate() {
301 if let Some((kind @ ('<' | '=' | '>'), length, _)) = *found
302 && length != MARKER_SIZE
303 {
304 longer.entry(length).or_default().push((index, kind));
305 }
306 }
307
308 let mut conflicts = Vec::new();
309 let mut state = State::Outside;
310 let mut size = MARKER_SIZE;
313 for (index, found) in markers.iter().enumerate() {
314 let number = index + 1;
315 let Some((kind, length, label)) = *found else {
316 continue;
317 };
318 match state {
319 State::Outside if kind == '<' && length != MARKER_SIZE => {
324 if !closes(&longer, index, length) {
325 continue;
326 }
327 size = length;
328 }
329 State::Outside if kind == '<' => size = length,
330 State::Outside => {}
331 _ if length != size => continue,
332 _ => {}
333 }
334 let label = label.map(str::to_string);
335 state = match (state, kind) {
336 (State::Outside, '<') => {
337 if conflicts.len() == MAX_CONFLICTS {
338 return Err(ConflictError::at(
339 number,
340 format!("more than {MAX_CONFLICTS} conflicts"),
341 ));
342 }
343 State::Ours {
344 start: number,
345 label,
346 }
347 }
348 (State::Outside, _) => State::Outside,
350 (
351 State::Ours { start, .. }
352 | State::Base {
353 ours:
354 ConflictSide {
355 marker_line: start, ..
356 },
357 ..
358 }
359 | State::Theirs {
360 ours:
361 ConflictSide {
362 marker_line: start, ..
363 },
364 ..
365 },
366 '<',
367 ) => {
368 return Err(ConflictError::at(
369 number,
370 format!(
371 "`<<<<<<<` inside the conflict opened at line {start}; \
372 it has no `>>>>>>>`"
373 ),
374 ));
375 }
376 (State::Ours { start, label: ours }, '|') => State::Base {
377 ours: ConflictSide {
378 label: ours,
379 marker_line: start,
380 lines: start..index,
381 },
382 start: number,
383 label,
384 },
385 (State::Ours { start, label: ours }, '=') => State::Theirs {
386 ours: ConflictSide {
387 label: ours,
388 marker_line: start,
389 lines: start..index,
390 },
391 base: None,
392 start: number,
393 },
394 (
395 State::Base {
396 ours,
397 start,
398 label: base,
399 },
400 '=',
401 ) => State::Theirs {
402 ours,
403 base: Some(ConflictSide {
404 label: base,
405 marker_line: start,
406 lines: start..index,
407 }),
408 start: number,
409 },
410 (State::Base { ours, .. }, '|') => {
411 return Err(ConflictError::at(
412 number,
413 format!(
414 "a second `|||||||` in the conflict opened at line {}",
415 ours.marker_line
416 ),
417 ));
418 }
419 (State::Ours { start, .. }, _) => {
420 return Err(ConflictError::at(
421 number,
422 format!(
423 "`>>>>>>>` before `=======` in the conflict opened at line {start}"
424 ),
425 ));
426 }
427 (State::Base { ours, .. }, _) => {
428 return Err(ConflictError::at(
429 number,
430 format!(
431 "`>>>>>>>` before `=======` in the conflict opened at line {}",
432 ours.marker_line
433 ),
434 ));
435 }
436 (State::Theirs { ours, base, start }, '>') => {
437 conflicts.push(Conflict {
438 number: conflicts.len() + 1,
439 ours,
440 base,
441 theirs: ConflictSide {
442 label,
443 marker_line: start,
444 lines: start..index,
445 },
446 end_line: number,
447 });
448 State::Outside
449 }
450 (State::Theirs { ours, .. }, '=') => {
451 return Err(ConflictError::at(
452 number,
453 format!(
454 "a second `=======` in the conflict opened at line {}",
455 ours.marker_line
456 ),
457 ));
458 }
459 (State::Theirs { ours, .. }, _) => {
460 return Err(ConflictError::at(
461 number,
462 format!(
463 "`|||||||` after `=======` in the conflict opened at line {}",
464 ours.marker_line
465 ),
466 ));
467 }
468 };
469 }
470 match state {
471 State::Outside => Ok(ConflictFile {
472 lines,
473 conflicts,
474 crlf,
475 }),
476 State::Ours { start, .. } => Err(unterminated(start)),
477 State::Base { ours, .. } | State::Theirs { ours, .. } => {
478 Err(unterminated(ours.marker_line))
479 }
480 }
481 }
482
483 pub fn lines(&self) -> &[String] {
485 &self.lines
486 }
487
488 pub fn conflicts(&self) -> &[Conflict] {
490 &self.conflicts
491 }
492
493 pub fn has_conflicts(&self) -> bool {
495 !self.conflicts.is_empty()
496 }
497
498 pub fn is_crlf(&self) -> bool {
500 self.crlf
501 }
502
503 pub fn text(&self, side: &ConflictSide) -> String {
505 self.lines[side.lines.clone()].join("\n")
506 }
507}
508
509fn crop(row: Vec<Segment>, width: usize) -> Vec<Segment> {
511 if row.iter().map(Segment::cell_length).sum::<usize>() > width {
512 Segment::adjust_line_length(&row, width, None)
513 } else {
514 row
515 }
516}
517
518fn unterminated(start: usize) -> ConflictError {
519 ConflictError::at(
520 start,
521 "this conflict is never closed: no `>>>>>>>` before the end of the input",
522 )
523}
524
525#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
527pub enum ConflictLayout {
528 #[default]
531 Auto,
532 SideBySide,
534 Stacked,
536}
537
538#[derive(Clone, Debug)]
561#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
562pub struct ConflictView {
563 file: ConflictFile,
564 language: Option<String>,
565 path: Option<String>,
566 layout: ConflictLayout,
567 context: usize,
568 line_numbers: bool,
569 wrap: bool,
570 base: bool,
571}
572
573struct Line {
575 pick: Option<Pick>,
576 number: Option<usize>,
577 text: Text,
578}
579
580struct Shown {
582 before: Range<usize>,
583 after: Range<usize>,
584}
585
586impl ConflictView {
587 pub fn new(file: ConflictFile) -> Self {
589 ConflictView {
590 file,
591 language: None,
592 path: None,
593 layout: ConflictLayout::Auto,
594 context: 3,
595 line_numbers: true,
596 wrap: true,
597 base: true,
598 }
599 }
600
601 pub fn parse(text: &str) -> Result<Self, ConflictError> {
603 ConflictFile::parse(text).map(Self::new)
604 }
605
606 pub fn file(&self) -> &ConflictFile {
608 &self.file
609 }
610
611 pub fn language(mut self, language: impl Into<String>) -> Self {
613 self.language = Some(language.into());
614 self
615 }
616
617 pub fn path(mut self, path: impl Into<String>) -> Self {
619 self.path = Some(path.into());
620 self
621 }
622
623 pub fn layout(mut self, layout: ConflictLayout) -> Self {
625 self.layout = layout;
626 self
627 }
628
629 pub fn context(mut self, lines: usize) -> Self {
631 self.context = lines;
632 self
633 }
634
635 pub fn line_numbers(mut self, show: bool) -> Self {
637 self.line_numbers = show;
638 self
639 }
640
641 pub fn wrap(mut self, wrap: bool) -> Self {
643 self.wrap = wrap;
644 self
645 }
646
647 pub fn base(mut self, show: bool) -> Self {
649 self.base = show;
650 self
651 }
652
653 fn resolved_language(&self) -> Option<String> {
654 self.language
655 .clone()
656 .or_else(|| self.path.as_deref().map(language_for_path))
657 }
658
659 fn shown(&self) -> Vec<Shown> {
662 let conflicts = self.file.conflicts();
663 let mut out = Vec::with_capacity(conflicts.len());
664 let mut shown_until = 0;
665 for (i, conflict) in conflicts.iter().enumerate() {
666 let span = conflict.span();
667 let before = span.start.saturating_sub(self.context).max(shown_until)..span.start;
668 let limit = conflicts
669 .get(i + 1)
670 .map_or(self.file.lines.len(), |next| next.span().start);
671 let after = span.end..span.end.saturating_add(self.context).min(limit);
672 shown_until = after.end;
673 out.push(Shown { before, after });
674 }
675 out
676 }
677
678 fn texts(&self, console: Option<&Console>) -> Vec<Option<Text>> {
683 let size: usize = self.file.lines.iter().map(|l| l.len() + 1).sum();
684 let language = self.resolved_language().filter(|_| size <= HIGHLIGHT_LIMIT);
685 let lines = &self.file.lines;
686 let mut out: Vec<Option<Text>> = vec![None; lines.len()];
687 let shown = self.shown();
688 let mut wanted = vec![false; lines.len()];
689 for (conflict, shown) in self.file.conflicts().iter().zip(&shown) {
690 for i in shown.before.clone().chain(shown.after.clone()) {
691 wanted[i] = true;
692 }
693 for (_, side) in conflict.sides() {
694 for i in side.lines.clone() {
695 wanted[i] = true;
696 }
697 }
698 }
699 for pick in [Pick::Ours, Pick::Base, Pick::Theirs] {
700 if pick == Pick::Base && !self.file.conflicts().iter().any(|c| c.base.is_some()) {
701 continue;
702 }
703 let mut version: Vec<usize> = Vec::new();
705 let mut next = 0;
706 for conflict in self.file.conflicts() {
707 let span = conflict.span();
708 version.extend(next..span.start);
709 let side = match pick {
710 Pick::Ours => Some(&conflict.ours),
711 Pick::Base => conflict.base.as_ref(),
712 Pick::Theirs => Some(&conflict.theirs),
713 };
714 if let Some(side) = side {
715 version.extend(side.lines.clone());
716 }
717 next = span.end;
718 }
719 version.extend(next..lines.len());
720 if !version.iter().any(|&i| wanted[i] && out[i].is_none()) {
721 continue;
722 }
723 let mut code = String::new();
724 for &i in &version {
725 code.push_str(&lines[i]);
726 code.push('\n');
727 }
728 let highlighted = highlight_lines(&code, language.as_deref(), console);
729 for (&i, text) in version.iter().zip(highlighted) {
730 if wanted[i] && out[i].is_none() {
731 out[i] = Some(text);
732 }
733 }
734 }
735 out
736 }
737
738 fn digits(&self) -> usize {
739 self.file.lines.len().max(1).to_string().len()
740 }
741
742 fn gutter_width(&self) -> usize {
743 if self.line_numbers {
744 self.digits() + 3
745 } else {
746 2
747 }
748 }
749
750 fn sides_shown<'c>(&self, conflict: &'c Conflict) -> Vec<(Pick, &'c ConflictSide)> {
751 conflict
752 .sides()
753 .into_iter()
754 .filter(|(pick, _)| self.base || *pick != Pick::Base)
755 .collect()
756 }
757
758 fn side_by_side(&self, width: usize) -> bool {
759 if self.layout == ConflictLayout::Stacked {
760 return false;
761 }
762 let columns = self
763 .file
764 .conflicts()
765 .iter()
766 .map(|c| self.sides_shown(c).len())
767 .max()
768 .unwrap_or(2);
769 let per_column = width.saturating_sub(3 * (columns - 1)) / columns;
770 match self.layout {
771 ConflictLayout::SideBySide => per_column > self.gutter_width(),
774 _ => per_column >= self.gutter_width() + SIDE_BY_SIDE_MIN,
775 }
776 }
777
778 fn line(&self, texts: &[Option<Text>], pick: Option<Pick>, index: usize) -> Line {
779 Line {
780 pick,
781 number: Some(index + 1),
782 text: texts[index]
783 .clone()
784 .unwrap_or_else(|| Text::new(self.file.lines[index].as_str())),
785 }
786 }
787
788 fn line_rows(&self, console: &Console, line: &Line, width: usize) -> Vec<Vec<Segment>> {
790 let digits = self.digits();
791 let numbers = self.line_numbers && width > self.gutter_width();
793 let gutter = if numbers { self.gutter_width() } else { 2 };
794 let (base, marker) = match line.pick {
795 Some(pick) => (style(console, pick.style_key()), pick.marker()),
796 None => (style(console, "diff.context"), ' '),
797 };
798 let content = width.saturating_sub(gutter).max(1);
799 wrap_text(console, &line.text, &base, content, self.wrap)
800 .into_iter()
801 .enumerate()
802 .map(|(i, text)| {
803 let mut row = Vec::new();
804 if numbers {
805 let number = match (i, line.number) {
806 (0, Some(n)) => format!("{n:>digits$} "),
807 _ => " ".repeat(digits + 1),
808 };
809 row.push(Segment::new(
810 number,
811 Some(style(console, "diff.line_number")),
812 ));
813 }
814 let marker = if i == 0 { marker } else { ' ' };
815 row.push(Segment::new(format!("{marker} "), Some(base.clone())));
816 row.extend(text);
817 crop(row, width)
818 })
819 .collect()
820 }
821
822 fn heading(&self, console: &Console, pick: Pick, side: &ConflictSide) -> Text {
824 let style =
825 style(console, pick.style_key()).combine(&style(console, "diff.conflict.label"));
826 let mut heading = pick.name().to_string();
827 if let Some(label) = &side.label {
828 heading.push_str(": ");
829 heading.push_str(&crate::sanitize_terminal_controls(label));
830 }
831 Text::styled(heading, style)
832 }
833
834 fn rows(&self, console: &Console, width: usize) -> Vec<Vec<Segment>> {
835 let conflicts = self.file.conflicts();
836 if conflicts.is_empty() {
837 return banner("no conflicts", style(console, "diff.line_number"), width);
838 }
839 let texts = self.texts(Some(console));
840 let side_by_side = self.side_by_side(width);
841 let indent = if self.line_numbers {
842 self.digits() + 1
843 } else {
844 0
845 };
846 let mut out = Vec::new();
847 for (conflict, shown) in conflicts.iter().zip(self.shown()) {
848 out.extend(banner(
849 &format!(
850 "conflict {} of {}, lines {}-{}",
851 conflict.number,
852 conflicts.len(),
853 conflict.start_line(),
854 conflict.end_line
855 ),
856 style(console, "diff.hunk"),
857 width,
858 ));
859 for i in shown.before.clone() {
860 out.extend(
861 self.line_rows(console, &self.line(&texts, None, i), width)
862 .into_iter()
863 .map(trim_end),
864 );
865 }
866 let sides = self.sides_shown(conflict);
867 let empty = |pick: Pick| Line {
868 pick: Some(pick),
869 number: None,
870 text: Text::styled("(empty)", style(console, "diff.line_number")),
871 };
872 let side_lines = |pick: Pick, side: &ConflictSide| -> Vec<Line> {
873 if side.lines.is_empty() {
874 vec![empty(pick)]
875 } else {
876 side.lines
877 .clone()
878 .map(|i| self.line(&texts, Some(pick), i))
879 .collect()
880 }
881 };
882 if side_by_side {
883 let divider = if console.ascii_only() { " | " } else { " │ " };
884 let div_style = style(console, "diff.line_number");
885 let inner = width.saturating_sub(3 * (sides.len() - 1));
886 let widths: Vec<usize> = (0..sides.len())
887 .map(|k| inner / sides.len() + usize::from(k < inner % sides.len()))
888 .collect();
889 let columns: Vec<Vec<Vec<Segment>>> = sides
890 .iter()
891 .zip(&widths)
892 .map(|((pick, side), &w)| {
893 let mut rows: Vec<Vec<Segment>> = Vec::new();
894 let heading = self.heading(console, *pick, side);
895 let mut first = vec![Segment::new(" ".repeat(indent.min(w)), None)];
896 first.extend(
897 wrap_text(
898 console,
899 &heading,
900 &Style::new(),
901 w.saturating_sub(indent).max(1),
902 false,
903 )
904 .into_iter()
905 .next()
906 .unwrap_or_default(),
907 );
908 rows.push(crop(first, w));
909 for line in side_lines(*pick, side) {
910 rows.extend(self.line_rows(console, &line, w));
911 }
912 rows
913 })
914 .collect();
915 let height = columns.iter().map(Vec::len).max().unwrap_or(0);
916 for r in 0..height {
917 let mut row = Vec::new();
918 for (k, column) in columns.iter().enumerate() {
919 if k > 0 {
920 row.push(Segment::new(divider, Some(div_style.clone())));
921 }
922 let cells = column.get(r).cloned().unwrap_or_default();
923 if k + 1 < columns.len() {
924 row.extend(pad_to(cells, widths[k]));
925 } else {
926 row.extend(cells);
927 }
928 }
929 out.push(trim_end(row));
930 }
931 } else {
932 for (pick, side) in &sides {
933 let heading = self.heading(console, *pick, side);
934 for line in wrap_text(
935 console,
936 &heading,
937 &Style::new(),
938 width.saturating_sub(indent).max(1),
939 false,
940 )
941 .into_iter()
942 .take(1)
943 {
944 let mut row = vec![Segment::new(" ".repeat(indent), None)];
945 row.extend(line);
946 out.push(trim_end(row));
947 }
948 for line in side_lines(*pick, side) {
949 out.extend(
950 self.line_rows(console, &line, width)
951 .into_iter()
952 .map(trim_end),
953 );
954 }
955 }
956 }
957 for i in shown.after.clone() {
958 out.extend(
959 self.line_rows(console, &self.line(&texts, None, i), width)
960 .into_iter()
961 .map(trim_end),
962 );
963 }
964 }
965 out
966 }
967
968 fn natural(&self) -> (usize, usize) {
970 let lines = &self.file.lines;
971 let width = |i: usize| cell_len(&lines[i]);
972 let mut stacked = "no conflicts".len();
973 let mut side_by_side = 0;
974 for (conflict, shown) in self.file.conflicts().iter().zip(self.shown()) {
975 stacked = stacked.max(
976 format!(
977 "conflict {} of {}, lines {}-{}",
978 conflict.number,
979 self.file.conflicts().len(),
980 conflict.start_line(),
981 conflict.end_line
982 )
983 .len(),
984 );
985 for i in shown.before.chain(shown.after) {
986 stacked = stacked.max(self.gutter_width() + width(i));
987 }
988 let sides = self.sides_shown(conflict);
989 let mut total = 3 * (sides.len() - 1);
990 for (pick, side) in &sides {
991 let label = pick.name().len() + side.label.as_ref().map_or(0, |l| 2 + cell_len(l));
992 let widest = side
993 .lines
994 .clone()
995 .map(width)
996 .max()
997 .unwrap_or("(empty)".len());
998 let column = (self.gutter_width() + widest).max(self.gutter_width() + label);
999 stacked = stacked.max(column);
1000 total += column;
1001 }
1002 side_by_side = side_by_side.max(total);
1003 }
1004 (stacked, side_by_side)
1005 }
1006}
1007
1008impl Renderable for ConflictView {
1009 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1010 if options.max_width == 0 || options.height == Some(0) {
1011 return Vec::new();
1012 }
1013 join(self.rows(console, options.max_width), options.height)
1014 }
1015
1016 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
1017 let (stacked, side_by_side) = self.natural();
1018 let maximum = match self.layout {
1019 ConflictLayout::Stacked => stacked,
1020 ConflictLayout::SideBySide => side_by_side.max(stacked),
1021 ConflictLayout::Auto if self.side_by_side(side_by_side) => side_by_side.max(stacked),
1023 ConflictLayout::Auto => stacked,
1024 }
1025 .min(options.max_width);
1026 let minimum = (self.gutter_width() + 1).min(maximum);
1027 Measurement::new(minimum, maximum)
1028 }
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 use super::*;
1034
1035 #[test]
1036 fn markers_need_seven_or_more_characters() {
1037 assert_eq!(marker("<<<<<<< HEAD"), Some(('<', 7, Some("HEAD"))));
1038 assert_eq!(marker("<<<<<<<"), Some(('<', 7, None)));
1039 assert_eq!(marker("<<<<<<<< HEAD"), Some(('<', 8, Some("HEAD"))));
1042 assert_eq!(marker("<<<<<<"), None);
1043 assert_eq!(marker("<<<<<<<x"), None);
1044 assert_eq!(marker("======="), Some(('=', 7, None)));
1045 assert_eq!(marker("======= "), Some(('=', 7, None)));
1046 assert_eq!(marker("======= x"), None);
1047 assert_eq!(
1048 marker("||||||| merged common ancestors"),
1049 Some(('|', 7, Some("merged common ancestors")))
1050 );
1051 assert_eq!(marker("x<<<<<<<"), None);
1052 assert_eq!(marker(""), None);
1053 }
1054}