Skip to main content

rich_ext/diff/
conflict.rs

1//! Three-way merge conflicts: a parser for the markers `git merge` leaves in
2//! a file, and [`ConflictView`], which shows ours, base and theirs.
3//!
4//! A conflict is the block git writes when it cannot merge two changes:
5//!
6//! ```text
7//! <<<<<<< HEAD
8//! ours
9//! ||||||| base          (only with merge.conflictStyle diff3 or zdiff3)
10//! the common ancestor
11//! =======
12//! theirs
13//! >>>>>>> feature
14//! ```
15//!
16//! A marker is seven marker characters at the start of a line, followed by
17//! the end of the line or whitespace and an optional label, as git's own
18//! `rerere` reads them. Inside a conflict every marker has the opening
19//! marker's length, so a longer one is text, which is how git nests a
20//! conflict inside another (it lengthens the inner markers). A path's
21//! `conflict-marker-size` attribute makes git write longer markers
22//! throughout; a longer opening marker opens a conflict when a separator and
23//! a closing marker of the same length follow it, and is text otherwise.
24//! Outside a conflict only `<<<<<<<` means anything, so a Markdown heading
25//! underlined with `=======` is just text. Inside one, markers out of order,
26//! a second `<<<<<<<`, and a conflict the input never closes are errors that
27//! name the line; parsing never panics.
28
29use 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
40/// The largest input [`ConflictFile::parse`] accepts, in bytes (16 MiB).
41pub const MAX_CONFLICT_SOURCE: usize = 16 * 1024 * 1024;
42
43/// The most conflicts one file may hold.
44pub const MAX_CONFLICTS: usize = 10_000;
45
46/// The marker length git writes by default (`conflict-marker-size`).
47const MARKER_SIZE: usize = 7;
48
49/// Columns narrower than this many cells of text stack instead of sitting
50/// side by side, in [`ConflictLayout::Auto`].
51const SIDE_BY_SIDE_MIN: usize = 20;
52
53/// Files larger than this (1 MiB) are shown without syntax highlighting:
54/// each version is highlighted whole, and that is not worth it for a file
55/// whose conflicts are a few screens of it.
56const HIGHLIGHT_LIMIT: usize = 1024 * 1024;
57
58/// Why a file's conflict markers did not parse.
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct ConflictError {
61    /// The 1-based line the problem is on, or `None` for the input as a
62    /// whole (it is too large).
63    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/// One side of a conflict.
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct ConflictSide {
90    /// The text after the marker (`HEAD`, a branch, a commit), if any.
91    pub label: Option<String>,
92    /// The 1-based line of the marker that opens this side.
93    pub marker_line: usize,
94    /// The side's lines, as 0-based indices into [`ConflictFile::lines`].
95    pub lines: Range<usize>,
96}
97
98/// One conflict block.
99#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct Conflict {
101    /// 1-based, in file order.
102    pub number: usize,
103    /// Ours: between `<<<<<<<` and `|||||||` or `=======`.
104    pub ours: ConflictSide,
105    /// The common ancestor, in diff3 style: between `|||||||` and `=======`.
106    pub base: Option<ConflictSide>,
107    /// Theirs: between `=======` and `>>>>>>>`. Its `label` is the one on
108    /// the closing `>>>>>>>` marker.
109    pub theirs: ConflictSide,
110    /// The 1-based line of the closing `>>>>>>>` marker.
111    pub end_line: usize,
112}
113
114impl Conflict {
115    /// The 1-based line of the opening `<<<<<<<` marker.
116    pub fn start_line(&self) -> usize {
117        self.ours.marker_line
118    }
119
120    /// The block's lines, markers included, as 0-based indices.
121    pub fn span(&self) -> Range<usize> {
122        self.start_line() - 1..self.end_line
123    }
124
125    /// The sides in display order: ours, base (when present), theirs.
126    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/// Which side of a conflict.
137#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
138pub enum Pick {
139    Ours,
140    Base,
141    Theirs,
142}
143
144impl Pick {
145    /// `ours`, `base` or `theirs`.
146    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/// A file with conflict markers, parsed.
172///
173/// ```
174/// use rich_ext::diff::ConflictFile;
175///
176/// let file = ConflictFile::parse("a\n<<<<<<< HEAD\nb\n=======\nc\n>>>>>>> topic\nd\n").unwrap();
177/// let conflict = &file.conflicts()[0];
178/// assert_eq!(conflict.ours.label.as_deref(), Some("HEAD"));
179/// assert_eq!(file.text(&conflict.ours), "b");
180/// assert_eq!(file.text(&conflict.theirs), "c");
181/// assert_eq!((conflict.start_line(), conflict.end_line), (2, 6));
182/// ```
183#[derive(Clone, Debug, PartialEq, Eq)]
184pub struct ConflictFile {
185    lines: Vec<String>,
186    conflicts: Vec<Conflict>,
187    crlf: bool,
188}
189
190/// A marker on `line`: its character, its length and its label, when the
191/// line is one. git writes [`MARKER_SIZE`] characters unless a path's
192/// `conflict-marker-size` attribute asks for more, so any run of at least
193/// that many counts; [`ConflictFile::parse`] holds a conflict's markers to
194/// the length its opening marker has.
195fn 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                // git writes the separator bare; text after one is content.
212                ('=', false) => None,
213                (_, false) => Some((first, length, Some(label))),
214            }
215        }
216        Some(_) => None,
217    }
218}
219
220/// The markers of each length other than git's own: per length, the `<`,
221/// `=` and `>` markers in file order, as `(line index, kind)`.
222type LongerMarkers = std::collections::HashMap<usize, Vec<(usize, char)>>;
223
224/// Whether the opening marker of `length` at `open` is followed by a
225/// separator and then a closing marker of the same length, before another
226/// opening marker of that length. Only the markers of that length are
227/// walked, and the walk stops at the next opening one, so the walks from
228/// all the opening markers of a length cover its markers once between them.
229fn 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    /// Parse `text`. Lines may end in `\n` or `\r\n`; a leading byte-order
266    /// mark is dropped. Fails on input over [`MAX_CONFLICT_SOURCE`] bytes,
267    /// more than [`MAX_CONFLICTS`] conflicts, markers out of order, and a
268    /// conflict left open at the end, naming the line.
269    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        // Every line is measured once, and the markers of other lengths
296        // are indexed by length for `closes`.
297        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        // The length of the open conflict's markers: a marker of any other
311        // length inside it is content.
312        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                // git's own size opens a conflict as before. A longer one
320                // (a `conflict-marker-size` attribute) is a conflict only
321                // when a separator and a closing marker of the same length
322                // follow; otherwise the line is text.
323                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                // Outside a conflict, only an opening marker is one.
349                (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    /// Every line of the file, without terminators.
484    pub fn lines(&self) -> &[String] {
485        &self.lines
486    }
487
488    /// The conflicts, in file order.
489    pub fn conflicts(&self) -> &[Conflict] {
490        &self.conflicts
491    }
492
493    /// Whether the file has any conflict.
494    pub fn has_conflicts(&self) -> bool {
495        !self.conflicts.is_empty()
496    }
497
498    /// Whether any line ended in `\r\n`.
499    pub fn is_crlf(&self) -> bool {
500        self.crlf
501    }
502
503    /// A side's lines joined with `\n` (no trailing newline).
504    pub fn text(&self, side: &ConflictSide) -> String {
505        self.lines[side.lines.clone()].join("\n")
506    }
507}
508
509/// `row` cut to `width` cells when it is wider.
510fn 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/// How a [`ConflictView`] lays out the sides.
526#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
527pub enum ConflictLayout {
528    /// Side by side when every column gets at least 20 cells of text,
529    /// stacked otherwise (the default).
530    #[default]
531    Auto,
532    /// Ours, base and theirs in columns.
533    SideBySide,
534    /// One side under another.
535    Stacked,
536}
537
538/// A file's merge conflicts as a renderable: each conflict numbered, with
539/// ours, base (when the markers have one) and theirs side by side or
540/// stacked, syntax highlighted, between a few lines of surrounding context.
541///
542/// Line numbers are the file's, so they lead back to the markers. With
543/// colour off every line keeps a marker: `<` ours, `|` base, `>` theirs.
544/// Styles come from the theme's `diff.conflict.ours`, `diff.conflict.base`
545/// and `diff.conflict.theirs` (see [`STYLES`](super::STYLES)).
546///
547/// ```
548/// use rich::Console;
549/// use rich_ext::diff::{ConflictLayout, ConflictView};
550///
551/// let view = ConflictView::parse("<<<<<<< HEAD\nb\n=======\nc\n>>>>>>> topic\n")
552///     .unwrap()
553///     .layout(ConflictLayout::Stacked);
554/// let console = Console::builder().width(40).no_color(true).build();
555/// assert_eq!(
556///     console.render_to_string(&view),
557///     "conflict 1 of 1, lines 1-5\n  ours: HEAD\n2 < b\n  theirs: topic\n4 > c"
558/// );
559/// ```
560#[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
573/// One displayed line.
574struct Line {
575    pick: Option<Pick>,
576    number: Option<usize>,
577    text: Text,
578}
579
580/// What to draw for one conflict.
581struct Shown {
582    before: Range<usize>,
583    after: Range<usize>,
584}
585
586impl ConflictView {
587    /// View a parsed file.
588    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    /// Parse `text` and view it.
602    pub fn parse(text: &str) -> Result<Self, ConflictError> {
603        ConflictFile::parse(text).map(Self::new)
604    }
605
606    /// The parsed file.
607    pub fn file(&self) -> &ConflictFile {
608        &self.file
609    }
610
611    /// The language (a name or extension core `Syntax` knows).
612    pub fn language(mut self, language: impl Into<String>) -> Self {
613        self.language = Some(language.into());
614        self
615    }
616
617    /// The file's path, for the language when none is set.
618    pub fn path(mut self, path: impl Into<String>) -> Self {
619        self.path = Some(path.into());
620        self
621    }
622
623    /// Side by side, stacked, or chosen by width (default).
624    pub fn layout(mut self, layout: ConflictLayout) -> Self {
625        self.layout = layout;
626        self
627    }
628
629    /// Unchanged lines shown before and after each conflict (default 3).
630    pub fn context(mut self, lines: usize) -> Self {
631        self.context = lines;
632        self
633    }
634
635    /// Show the file's line numbers (default on).
636    pub fn line_numbers(mut self, show: bool) -> Self {
637        self.line_numbers = show;
638        self
639    }
640
641    /// Wrap long lines (default) or truncate them with an ellipsis.
642    pub fn wrap(mut self, wrap: bool) -> Self {
643        self.wrap = wrap;
644        self
645    }
646
647    /// Show the diff3 base when the markers have one (default on).
648    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    /// The context ranges around each conflict: up to `context` lines on
660    /// each side, never overlapping a neighbour or what was already shown.
661    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    /// Every line the view shows, highlighted. The file is highlighted as
679    /// three whole versions (context with ours, with base, with theirs) so
680    /// multi-line constructs colour as they would in each. A file over
681    /// `HIGHLIGHT_LIMIT` bytes is shown plain.
682    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            // The indices of this version's lines, in order.
704            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            // Forced columns still fall back to stacked when the dividers,
772            // gutters and one cell of text do not fit in `width`.
773            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    /// A line's rows at `width`: gutter, marker, then the wrapped text.
789    fn line_rows(&self, console: &Console, line: &Line, width: usize) -> Vec<Vec<Segment>> {
790        let digits = self.digits();
791        // A column too narrow for the numbers and some text drops them.
792        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    /// A side's heading: `ours: HEAD`.
823    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    /// The widest line of each kind: `(context, per side shown)`.
969    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            // Wide enough for columns when they fit; stacked otherwise.
1022            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        // Longer runs are markers of that length (`conflict-marker-size`);
1040        // parsing decides whether one opens a conflict.
1041        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}