use std::ops::Range;
use similar::{Algorithm, DiffOp, capture_diff_slices};
const ALGORITHM: Algorithm = Algorithm::Myers;
const FIRST_NON_ASCII_BYTE: u8 = 0x80;
const LINE_END: u8 = b'\n';
#[derive(Clone, Copy)]
enum Level {
Line,
Word,
NonWord,
}
fn is_word_byte(byte: u8) -> bool {
return byte.is_ascii_alphanumeric() || byte == b'_' || byte >= FIRST_NON_ASCII_BYTE;
}
fn token_ranges(text: &[u8], level: Level) -> Vec<Range<usize>> {
match level {
Level::Line => {
let mut start: usize = 0;
return text
.split_inclusive(|byte| { return *byte == LINE_END; })
.map(|line| {
let range: Range<usize> = start..start + line.len();
start = range.end;
return range;
})
.collect();
}
Level::Word => {
let mut ranges: Vec<Range<usize>> = Vec::new();
let mut word_start: Option<usize> = None;
for (index, byte) in text.iter().enumerate() {
match (word_start, is_word_byte(*byte)) {
(None, true) => word_start = Some(index),
(Some(start), false) => {
ranges.push(start..index);
word_start = None;
}
_ => {}
}
}
if let Some(start) = word_start {
ranges.push(start..text.len());
}
return ranges;
}
Level::NonWord => {
return (0..text.len())
.filter(|index| { return !is_word_byte(text[*index]); })
.map(|index| { return index..index + 1; })
.collect();
}
}
}
#[derive(Clone, Copy)]
struct Gap<'a> {
base: &'a [u8],
ours: &'a [u8],
theirs: &'a [u8],
starts: [usize; 3],
}
enum Hunk<'a> {
Same(&'a [u8]),
Gap(Gap<'a>),
}
fn matched_tokens(base: &[&[u8]], other: &[&[u8]]) -> Vec<Option<usize>> {
let mut matches: Vec<Option<usize>> = vec![None; base.len()];
for operation in capture_diff_slices(ALGORITHM, base, other) {
if let DiffOp::Equal { old_index, new_index, len } = operation {
for offset in 0..len {
matches[old_index + offset] = Some(new_index + offset);
}
}
}
return matches;
}
fn align<'a>(base: &'a [u8], ours: &'a [u8], theirs: &'a [u8], level: Level) -> Vec<Hunk<'a>> {
let whole: Gap<'a> = Gap { base, ours, theirs, starts: [0, 0, 0] };
if base.is_empty() || ours.is_empty() || theirs.is_empty() {
return vec![Hunk::Gap(whole)];
}
let [base_ranges, ours_ranges, theirs_ranges] = [base, ours, theirs].map(|text| { return token_ranges(text, level); });
let slices = |text: &'a [u8], ranges: &[Range<usize>]| -> Vec<&'a [u8]> {
return ranges.iter().map(|range| { return &text[range.clone()]; }).collect();
};
let base_tokens: Vec<&[u8]> = slices(base, &base_ranges);
let to_ours: Vec<Option<usize>> = matched_tokens(&base_tokens, &slices(ours, &ours_ranges));
let to_theirs: Vec<Option<usize>> = matched_tokens(&base_tokens, &slices(theirs, &theirs_ranges));
let mut hunks: Vec<Hunk<'a>> = Vec::new();
let mut positions: [usize; 3] = [0, 0, 0];
for (index, base_range) in base_ranges.iter().enumerate() {
let (Some(ours_index), Some(theirs_index)) = (to_ours[index], to_theirs[index]) else { continue };
let (ours_range, theirs_range): (&Range<usize>, &Range<usize>) = (&ours_ranges[ours_index], &theirs_ranges[theirs_index]);
let gap: Gap<'a> = Gap {
base: &base[positions[0]..base_range.start],
ours: &ours[positions[1]..ours_range.start],
theirs: &theirs[positions[2]..theirs_range.start],
starts: positions,
};
if !(gap.base.is_empty() && gap.ours.is_empty() && gap.theirs.is_empty()) {
hunks.push(Hunk::Gap(gap));
}
hunks.push(Hunk::Same(&base[base_range.clone()]));
positions = [base_range.end, ours_range.end, theirs_range.end];
}
let tail: Gap<'a> = Gap {
base: &base[positions[0]..],
ours: &ours[positions[1]..],
theirs: &theirs[positions[2]..],
starts: positions,
};
if !(tail.base.is_empty() && tail.ours.is_empty() && tail.theirs.is_empty()) {
hunks.push(Hunk::Gap(tail));
}
return hunks;
}
fn trivial<'a>(gap: &Gap<'a>) -> Option<&'a [u8]> {
if gap.ours == gap.theirs || gap.theirs == gap.base {
return Some(gap.ours);
}
if gap.ours == gap.base {
return Some(gap.theirs);
}
return None;
}
fn merge_gap_by_word(gap: &Gap<'_>) -> Option<Vec<u8>> {
let mut merged: Vec<u8> = Vec::new();
for hunk in align(gap.base, gap.ours, gap.theirs, Level::Word) {
match hunk {
Hunk::Same(text) => merged.extend_from_slice(text),
Hunk::Gap(word_gap) => match trivial(&word_gap) {
Some(text) => merged.extend_from_slice(text),
None => {
for part in align(word_gap.base, word_gap.ours, word_gap.theirs, Level::NonWord) {
match part {
Hunk::Same(text) => merged.extend_from_slice(text),
Hunk::Gap(byte_gap) => merged.extend_from_slice(trivial(&byte_gap)?),
}
}
}
},
}
}
return Some(merged);
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LineSpan {
pub first: usize,
pub count: usize,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MergedPart {
pub base: LineSpan,
pub ours: LineSpan,
pub theirs: LineSpan,
pub merged: Vec<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WordMerge {
Resolved {
merged: Vec<u8>,
parts: Vec<MergedPart>,
},
Conflict {
unresolved: usize,
},
}
fn line_span(text: &[u8], start: usize, length: usize) -> LineSpan {
let before: usize = text[..start].iter().filter(|byte| { return **byte == LINE_END; }).count();
let count: usize = token_ranges(&text[start..start + length], Level::Line).len();
return LineSpan { first: before + 1, count };
}
pub fn merge_by_word(base: &[u8], ours: &[u8], theirs: &[u8]) -> WordMerge {
let mut merged: Vec<u8> = Vec::with_capacity(ours.len().max(theirs.len()));
let mut parts: Vec<MergedPart> = Vec::new();
let mut unresolved: usize = 0;
for hunk in align(base, ours, theirs, Level::Line) {
match hunk {
Hunk::Same(text) => merged.extend_from_slice(text),
Hunk::Gap(gap) => match trivial(&gap) {
Some(text) => merged.extend_from_slice(text),
None => match merge_gap_by_word(&gap) {
Some(text) => {
parts.push(MergedPart {
base: line_span(base, gap.starts[0], gap.base.len()),
ours: line_span(ours, gap.starts[1], gap.ours.len()),
theirs: line_span(theirs, gap.starts[2], gap.theirs.len()),
merged: text.clone(),
});
merged.extend_from_slice(&text);
}
None => unresolved += 1,
},
},
}
}
if unresolved == 0 {
return WordMerge::Resolved { merged, parts };
}
return WordMerge::Conflict { unresolved };
}
#[cfg(test)]
#[path = "word_merge_tests.rs"]
mod tests;