use std::borrow::Cow;
use itertools::Itertools;
use regex::Regex;
use crate::{
merge_result::MergeResult, parsed_merge::ParsedMerge, pcs::Revision, settings::DisplaySettings,
};
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub(crate) struct MergedText<'a> {
sections: Vec<MergeSection<'a>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum MergeSection<'a> {
Merged(Cow<'a, str>),
Conflict {
base: Cow<'a, str>,
left: Cow<'a, str>,
right: Cow<'a, str>,
},
}
impl<'a> MergedText<'a> {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn count_conflicts(&self) -> usize {
self.sections
.iter()
.filter(|section| matches!(section, MergeSection::Conflict { .. }))
.count()
}
pub(crate) fn conflict_mass(&self) -> usize {
self.sections
.iter()
.map(|section| match section {
MergeSection::Merged(_) => 0,
MergeSection::Conflict { base, left, right } => {
base.len() + left.len() + right.len()
}
})
.sum()
}
pub(crate) fn push_merged(&mut self, contents: Cow<'a, str>) {
if !contents.is_empty() {
self.sections.push(MergeSection::Merged(contents));
}
}
pub(crate) fn push_conflict(
&mut self,
base: Cow<'a, str>,
left: Cow<'a, str>,
right: Cow<'a, str>,
) {
if left == right || base == right {
self.push_merged(left);
} else if base == left {
self.push_merged(right);
} else {
self.sections
.push(MergeSection::Conflict { base, left, right });
}
}
pub(crate) fn push_line_based_merge(&mut self, parsed: &ParsedMerge<'a>, indentation: &str) {
let mut newline_found = false;
let sections = parsed.chunks.iter().map(|section| match section {
crate::parsed_merge::MergedChunk::Resolved { contents, .. } => {
let result = MergeSection::Merged(Self::reindent_line_based_merge(
contents,
indentation,
newline_found,
true,
));
newline_found = newline_found || contents.contains('\n');
result
}
crate::parsed_merge::MergedChunk::Conflict {
left, base, right, ..
} => {
let result = MergeSection::Conflict {
left: Self::reindent_line_based_merge(
left.unwrap_or_default(),
indentation,
false,
false,
),
base: Self::reindent_line_based_merge(
base.unwrap_or_default(),
indentation,
false,
false,
),
right: Self::reindent_line_based_merge(
right.unwrap_or_default(),
indentation,
false,
false,
),
};
newline_found = newline_found
|| left.is_some_and(|s| s.contains('\n'))
|| right.is_some_and(|s| s.contains('\n'))
|| base.is_some_and(|s| s.contains('\n'));
result
}
});
self.sections.extend(sections);
}
fn reindent_line_based_merge<'content>(
content: &'content str,
indentation: &str,
reindent_first: bool,
reindent_last: bool,
) -> Cow<'content, str> {
if indentation.is_empty() {
return Cow::Borrowed(content);
}
let mut reindented = content
.split('\n')
.enumerate()
.map(|(idx, line)| {
if line.is_empty() || (idx == 0 && !reindent_first) {
Cow::from(line)
} else {
Cow::from(format!("{indentation}{line}"))
}
})
.join("\n");
if reindent_last && reindented.ends_with('\n') {
reindented += indentation;
}
Cow::Owned(reindented)
}
pub(crate) fn reconstruct_revision(&self, revision: Revision) -> String {
self.sections
.iter()
.map(|section| match section {
MergeSection::Merged(contents) => contents.as_ref(),
MergeSection::Conflict { left, base, right } => match revision {
Revision::Base => base.as_ref(),
Revision::Left => left.as_ref(),
Revision::Right => right.as_ref(),
},
})
.collect()
}
pub(crate) fn render(&self, settings: &DisplaySettings) -> String {
if let Some(contents) = self
.sections
.iter()
.map(|section| {
if let MergeSection::Merged(contents) = section {
Some(contents.as_ref())
} else {
None
}
})
.collect()
{
return contents;
}
if settings.compact_or_default() {
self.render_compact(settings)
} else {
self.render_full_lines(settings)
}
}
fn render_full_lines(&self, settings: &DisplaySettings) -> String {
let mut output = String::new();
let mut base_buffer = String::new();
let mut left_buffer = String::new();
let mut right_buffer = String::new();
let mut gathering_conflict = false;
for section in &self.sections {
match section {
MergeSection::Merged(contents) => {
if !gathering_conflict {
output.push_str(contents);
continue;
}
if let Some((to_append, rest)) = contents.split_once('\n') {
left_buffer.push_str(to_append);
base_buffer.push_str(to_append);
right_buffer.push_str(to_append);
Self::render_conflict(
&base_buffer,
&left_buffer,
&right_buffer,
settings,
&mut output,
);
output.push_str(rest);
gathering_conflict = false;
} else if contents.trim().is_empty() {
if !base_buffer.trim().is_empty() {
base_buffer.push_str(contents);
}
if !left_buffer.trim().is_empty() {
left_buffer.push_str(contents);
}
if !right_buffer.trim().is_empty() {
right_buffer.push_str(contents);
}
} else {
base_buffer.push_str(contents);
left_buffer.push_str(contents);
right_buffer.push_str(contents);
}
}
MergeSection::Conflict { base, left, right } => {
if !gathering_conflict {
if output.ends_with('\n') || output.is_empty() {
base_buffer.clear();
left_buffer.clear();
right_buffer.clear();
} else {
let last_newline_index = output.rfind('\n');
let last_line = output.split_off(match last_newline_index {
Some(idx) => idx + 1,
None => 0,
});
base_buffer.clone_from(&last_line);
left_buffer.clone_from(&last_line);
right_buffer = last_line;
}
}
base_buffer.push_str(base);
left_buffer.push_str(left);
right_buffer.push_str(right);
let all_end_with_newline = (base_buffer.ends_with('\n')
|| base_buffer.trim().is_empty())
&& (left_buffer.ends_with('\n') || left_buffer.trim().is_empty())
&& (right_buffer.ends_with('\n') || right_buffer.trim().is_empty());
if all_end_with_newline {
Self::render_conflict(
&base_buffer,
&left_buffer,
&right_buffer,
settings,
&mut output,
);
}
gathering_conflict = !all_end_with_newline;
}
}
}
if gathering_conflict {
Self::render_conflict(
&base_buffer,
&left_buffer,
&right_buffer,
settings,
&mut output,
);
}
output
}
fn render_conflict(
base: &str,
left: &str,
right: &str,
settings: &DisplaySettings,
output: &mut String,
) {
Self::maybe_add_newline(output);
output.push_str(&settings.left_marker_or_default());
output.push('\n');
if !left.trim().is_empty() {
output.push_str(left);
}
if settings.diff3 {
Self::maybe_add_newline(output);
output.push_str(&settings.base_marker_or_default());
output.push('\n');
if !base.trim().is_empty() {
output.push_str(base);
}
}
Self::maybe_add_newline(output);
output.push_str(&settings.middle_marker_or_default());
output.push('\n');
if !right.trim().is_empty() {
output.push_str(right);
}
Self::maybe_add_newline(output);
output.push_str(&settings.right_marker_or_default());
output.push('\n');
}
fn render_compact(&self, settings: &DisplaySettings) -> String {
let mut output = String::new();
let mut last_was_conflict = false;
let leading_whitespace_pattern = Regex::new("^[\t ]*\n").expect("Invalid regex");
let trailing_whitespace_pattern = Regex::new("[\t ]+$").expect("Invalid regex");
for section in &self.sections {
match section {
MergeSection::Merged(contents) => {
if last_was_conflict {
output.push_str(&leading_whitespace_pattern.replace(contents, ""));
} else {
output.push_str(contents);
}
last_was_conflict = false;
}
MergeSection::Conflict { base, left, right } => {
if let Some(occurrence) = trailing_whitespace_pattern.find(&output) {
let whitespace_to_prepend = output.split_off(occurrence.start());
let new_base = if base.is_empty() {
base
} else {
&(whitespace_to_prepend.clone() + base).into()
};
let new_left = if left.is_empty() {
left
} else {
&(whitespace_to_prepend.clone() + left).into()
};
let new_right = if right.is_empty() {
right
} else {
&(whitespace_to_prepend + right).into()
};
Self::render_conflict(new_base, new_left, new_right, settings, &mut output);
} else {
Self::render_conflict(base, left, right, settings, &mut output);
}
last_was_conflict = true;
}
}
}
output
}
fn maybe_add_newline(output: &mut String) {
if !output.ends_with('\n') && !output.is_empty() {
output.push('\n');
}
}
#[allow(clippy::wrong_self_convention)]
pub(crate) fn into_merge_result(
&self,
settings: &DisplaySettings,
method: &'static str,
) -> MergeResult {
let rendered = self.render(settings);
MergeResult {
contents: rendered,
conflict_count: self.count_conflicts(),
conflict_mass: self.conflict_mass(),
method,
has_additional_issues: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn merged(contents: &str) -> MergeSection<'_> {
MergeSection::Merged(contents.into())
}
fn conflict<'a>(base: &'a str, left: &'a str, right: &'a str) -> MergeSection<'a> {
MergeSection::Conflict {
base: base.into(),
left: left.into(),
right: right.into(),
}
}
#[test]
fn compact_mode() {
let merged_text = MergedText {
sections: vec![
merged("class Cls {\n "),
conflict("", "public", "private"),
merged(" "),
merged("void method() {}\n}"),
],
};
let expected_compact = "\
class Cls {
<<<<<<< LEFT
public
||||||| BASE
=======
private
>>>>>>> RIGHT
void method() {}
}";
assert_eq!(
merged_text.render(&DisplaySettings::default_compact()),
expected_compact
);
let expected_full_line = "\
class Cls {
<<<<<<< LEFT
public void method() {}
||||||| BASE
void method() {}
=======
private void method() {}
>>>>>>> RIGHT
}";
assert_eq!(
merged_text.render(&DisplaySettings::default()),
expected_full_line
);
}
#[test]
fn multiple_conflicts_on_same_line() {
let merged_text = MergedText {
sections: vec![
merged("let's start "),
conflict("ho", "hi", "ha"),
merged(" to "),
conflict("you", "everyone", "me"),
merged("!"),
],
};
let expected_full_line = "\
<<<<<<< LEFT
let's start hi to everyone!
||||||| BASE
let's start ho to you!
=======
let's start ha to me!
>>>>>>> RIGHT
";
assert_eq!(
merged_text.render(&DisplaySettings::default()),
expected_full_line
);
}
#[test]
fn spurious_conflict() {
let mut merged_text = MergedText::new();
merged_text.push_merged("let's start ".into());
merged_text.push_conflict("tomorrow".into(), "now".into(), "now".into());
merged_text.push_merged(", as it seems we all agree".into());
let expected_full_line = "let's start now, as it seems we all agree";
assert_eq!(
merged_text.render(&DisplaySettings::default()),
expected_full_line
);
}
#[test]
fn space_after_conflict_base_empty() {
let merged_text = MergedText {
sections: vec![conflict("", "here", "there"), merged(" "), merged("we go")],
};
let expected = "\
<<<<<<< LEFT
here we go
||||||| BASE
we go
=======
there we go
>>>>>>> RIGHT
";
assert_eq!(merged_text.render(&DisplaySettings::default()), expected);
}
#[test]
fn space_after_conflict_base_empty_next_incorrect() {
let merged_text = MergedText {
sections: vec![conflict("", "here", "there"), merged(" we go")],
};
let expected_wrong = "\
<<<<<<< LEFT
here we go
||||||| BASE
we go
=======
there we go
>>>>>>> RIGHT
";
assert_eq!(
merged_text.render(&DisplaySettings::default()),
expected_wrong
);
}
#[test]
fn space_after_conflict_base_empty_all_indented() {
let merged_text = MergedText {
sections: vec![
merged(" "),
conflict("", "here", "there"),
merged(" "),
merged("we go"),
],
};
let expected = "\
<<<<<<< LEFT
here we go
||||||| BASE
we go
=======
there we go
>>>>>>> RIGHT
";
assert_eq!(merged_text.render(&DisplaySettings::default()), expected);
}
#[test]
fn space_after_conflict_left_empty() {
let merged_text = MergedText {
sections: vec![conflict("here", "", "there"), merged(" "), merged("we go")],
};
let expected = "\
<<<<<<< LEFT
we go
||||||| BASE
here we go
=======
there we go
>>>>>>> RIGHT
";
assert_eq!(merged_text.render(&DisplaySettings::default()), expected);
}
#[test]
fn space_after_conflict_left_empty_all_indented() {
let merged_text = MergedText {
sections: vec![
merged(" "),
conflict("here", "", "there"),
merged(" "),
merged("we go"),
],
};
let expected = "\
<<<<<<< LEFT
we go
||||||| BASE
here we go
=======
there we go
>>>>>>> RIGHT
";
assert_eq!(merged_text.render(&DisplaySettings::default()), expected);
}
#[test]
fn reconstruct_revision() {
let merged_text = MergedText {
sections: vec![
merged("let's say "),
conflict("ho", "hi", "ha"),
merged(" to "),
conflict("you", "everyone", "me"),
merged("!"),
],
};
assert_eq!(
merged_text.reconstruct_revision(Revision::Base),
"let's say ho to you!"
);
assert_eq!(
merged_text.reconstruct_revision(Revision::Left),
"let's say hi to everyone!"
);
assert_eq!(
merged_text.reconstruct_revision(Revision::Right),
"let's say ha to me!"
);
}
#[test]
fn conflict_count_and_mass() {
let merged_text = MergedText {
sections: vec![
merged("let's say "),
conflict("ho", "hi", "ha"),
merged(" to "),
conflict("you", "everyone", "me"),
merged("!"),
],
};
assert_eq!(merged_text.conflict_mass(), 19);
assert_eq!(merged_text.count_conflicts(), 2);
}
#[test]
fn avoid_spurious_conflicts() {
let mut merged_text = MergedText::default();
let mut push_conflict = |base, left, right| {
merged_text.push_conflict(
Cow::Borrowed(base),
Cow::Borrowed(left),
Cow::Borrowed(right),
);
};
push_conflict("foo", "foo", "bar");
push_conflict("foo", "bar", "bar");
push_conflict("foo", "bar", "foo");
assert_eq!(merged_text.render(&DisplaySettings::default()), "barbarbar");
}
}