use tree_sitter::{Node, Point, Range};
use crate::{
code::{Code, metadata::compute_row_byte_lengths},
diff::{
ASTDiff, ASTMappingOperation, ASTMappingReason, NodeCache, nodes,
text_range::{SourceText, TextRange},
},
};
mod plain_text_diff;
mod render_options;
mod summary;
pub use plain_text_diff::{
LineDiffCore, WholeFileClass, line_diff_core, plain_text_line_diff, whole_file_text_class,
};
pub use render_options::{
RangeMatch, RenderOptions, TextOperation, is_structural_only, ranges_for_options,
};
pub use summary::{
ChangeCounts, DiffSummary, change_counts, is_comment_only_diff, line_operations,
summarize_diff, summarize_diff_with_comment_check,
};
use summary::whitespace_stripped_equal;
#[derive(Debug, Clone, Default)]
pub struct TextDiff {
before_ranges: Vec<RangeMatch>,
after_ranges: Vec<RangeMatch>,
}
pub(crate) fn own_content(node: Node, source: &[u8]) -> String {
let mut gap_bytes: Vec<u8> = Vec::new();
let mut pos = node.start_byte();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.start_byte() > pos {
gap_bytes.extend_from_slice(&source[pos..child.start_byte()]);
}
pos = pos.max(child.end_byte());
}
if node.end_byte() > pos {
gap_bytes.extend_from_slice(&source[pos..node.end_byte()]);
}
String::from_utf8_lossy(&gap_bytes).into_owned()
}
fn own_content_span(node: Node) -> Option<(Point, usize, usize)> {
let mut pos = node.start_byte();
let mut gap_start_point = node.start_position();
let mut gap: Option<(Point, usize, usize)> = None;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.start_byte() > pos {
if gap.is_some() {
return None;
}
gap = Some((gap_start_point, pos, child.start_byte()));
}
pos = pos.max(child.end_byte());
gap_start_point = child.end_position();
}
if node.end_byte() > pos {
if gap.is_some() {
return None;
}
gap = Some((gap_start_point, pos, node.end_byte()));
}
gap
}
fn common_prefix_byte_len(a: &str, b: &str) -> usize {
let mut len = 0;
let mut a_chars = a.char_indices();
let mut b_chars = b.chars();
loop {
match (a_chars.next(), b_chars.next()) {
(Some((i, ca)), Some(cb)) if ca == cb => len = i + ca.len_utf8(),
_ => break,
}
}
len
}
fn common_suffix_byte_len(a: &str, b: &str) -> usize {
let mut len = 0;
let mut a_chars = a.chars().rev();
let mut b_chars = b.chars().rev();
loop {
match (a_chars.next(), b_chars.next()) {
(Some(ca), Some(cb)) if ca == cb => len += ca.len_utf8(),
_ => break,
}
}
len
}
fn point_at_byte_offset(text: &str, start: Point, offset: usize) -> Point {
let mut row = start.row;
let mut column = start.column;
for &b in &text.as_bytes()[..offset] {
if b == b'\n' {
row += 1;
column = 0;
} else {
column += 1;
}
}
Point { row, column }
}
fn text_range_from_points(start: Point, end: Point, columns_per_row: &[usize]) -> TextRange {
let ts_range = Range {
start_byte: 0,
end_byte: 0,
start_point: start,
end_point: end,
};
TextRange::from_treesitter_range(ts_range, columns_per_row)
}
struct TextSpan<'a> {
text: &'a str,
start: Point,
columns: &'a [usize],
}
fn is_content_node(kind: &str) -> bool {
nodes::is_literal_kind(kind)
|| nodes::is_comment(kind)
|| kind.contains("string")
|| kind.contains("comment")
|| kind.contains("raw_text")
}
pub(crate) const WHOLE_TOKENS: &[&str] = &[
"<",
">",
"=",
"==",
"===",
"!=",
"!==",
"<=",
">=",
"+",
"-",
"++",
"--",
"+=",
"-=",
"*=",
">>",
"<<",
"/>",
".=",
"true",
"false",
"private",
"protected",
];
fn is_renamed_identifier(kind: &str, a: &str, b: &str) -> bool {
let identifier = |text: &str| {
let mut chars = text.chars();
chars
.next()
.is_some_and(|c| c.is_alphabetic() || c == '_' || c == '$')
&& chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
};
!is_content_node(kind) && kind != "text" && identifier(a) && identifier(b)
}
fn splits_a_whole_token(a: &str, b: &str) -> bool {
[a, b]
.iter()
.any(|text| text.len() > 1 && WHOLE_TOKENS.contains(text))
}
fn intra_node_update_ranges(
last_non_move_range: &mut TextRange,
whole_source_range: TextRange,
source: TextSpan,
destination: TextSpan,
source_is_before: bool,
content_node: bool,
whole_pair_updates: bool,
) -> Vec<RangeMatch> {
let prefix_len = common_prefix_byte_len(source.text, destination.text);
let suffix_len =
common_suffix_byte_len(&source.text[prefix_len..], &destination.text[prefix_len..]);
if whole_pair_updates || (prefix_len == 0 && suffix_len == 0) {
return vec![advance_and_build_range_with_source(
last_non_move_range,
whole_source_range,
TextOperation::Update,
)];
}
let mut result = Vec::with_capacity(3);
if prefix_len > 0 {
let source_end = point_at_byte_offset(source.text, source.start, prefix_len);
let destination_end = point_at_byte_offset(destination.text, destination.start, prefix_len);
result.push(RangeMatch {
source: text_range_from_points(source.start, source_end, source.columns),
destination: text_range_from_points(
destination.start,
destination_end,
destination.columns,
),
operation: TextOperation::Identical,
});
}
let source_mid_len = source.text.len() - prefix_len - suffix_len;
let destination_mid_len = destination.text.len() - prefix_len - suffix_len;
let middle_operation = match (source_mid_len, destination_mid_len, source_is_before) {
_ if !content_node => TextOperation::Update,
(0, _, true) | (_, 0, false) => TextOperation::Insert,
(0, _, false) | (_, 0, true) => TextOperation::Delete,
_ => TextOperation::Update,
};
if source_mid_len > 0 || destination_mid_len > 0 {
let source_mid_start = point_at_byte_offset(source.text, source.start, prefix_len);
let source_mid_end =
point_at_byte_offset(source.text, source.start, source.text.len() - suffix_len);
result.push(advance_and_build_range_with_source(
last_non_move_range,
text_range_from_points(source_mid_start, source_mid_end, source.columns),
middle_operation,
));
}
if suffix_len > 0 {
let source_start_point =
point_at_byte_offset(source.text, source.start, source.text.len() - suffix_len);
let source_end_point = point_at_byte_offset(source.text, source.start, source.text.len());
let destination_start_point = point_at_byte_offset(
destination.text,
destination.start,
destination.text.len() - suffix_len,
);
let destination_end_point =
point_at_byte_offset(destination.text, destination.start, destination.text.len());
result.push(RangeMatch {
source: text_range_from_points(source_start_point, source_end_point, source.columns),
destination: text_range_from_points(
destination_start_point,
destination_end_point,
destination.columns,
),
operation: TextOperation::Identical,
});
}
result
}
pub(crate) enum NodeChange<'c> {
Identical(Option<Node<'c>>),
PrunedSubtree(TextOperation),
Leaf(TextOperation),
Update(Option<Node<'c>>),
OwnContentChanged(Node<'c>),
Descend,
}
pub(crate) fn classify_node<'c>(
node: Node,
mapped_id: usize,
operation: &ASTMappingOperation,
node_cache: &'c NodeCache,
own_bytes: &[u8],
other_bytes: &[u8],
) -> NodeChange<'c> {
let counterpart = || node_cache.get_in_any(&mapped_id).copied();
match operation {
ASTMappingOperation::Identical => NodeChange::Identical(counterpart()),
ASTMappingOperation::DeleteWithChildren => NodeChange::PrunedSubtree(TextOperation::Delete),
ASTMappingOperation::InsertWithChildren => NodeChange::PrunedSubtree(TextOperation::Insert),
ASTMappingOperation::Delete if node.child_count() == 0 => {
NodeChange::Leaf(TextOperation::Delete)
}
ASTMappingOperation::Insert if node.child_count() == 0 => {
NodeChange::Leaf(TextOperation::Insert)
}
ASTMappingOperation::Update => NodeChange::Update(counterpart()),
ASTMappingOperation::MatchButNotIdentical => match counterpart() {
Some(other)
if own_content_span(node).is_some()
&& own_content_span(other).is_some()
&& !whitespace_stripped_equal(
&own_content(node, own_bytes),
&own_content(other, other_bytes),
) =>
{
NodeChange::OwnContentChanged(other)
}
_ => NodeChange::Descend,
},
_ => NodeChange::Descend,
}
}
struct RangeWalk<'a> {
source: &'a Code,
destination: &'a Code,
diff: &'a ASTDiff,
node_cache: &'a NodeCache<'a>,
source_columns: Vec<usize>,
destination_columns: Vec<usize>,
source_text: SourceText<'a>,
destination_text: SourceText<'a>,
source_is_before: bool,
options: RenderOptions,
last_non_move_range: TextRange,
ranges: Vec<RangeMatch>,
current_range: RangeMatch,
}
fn ranges(
source: &Code,
destination: &Code,
diff: &ASTDiff,
node_cache: &NodeCache,
source_is_before: bool,
options: RenderOptions,
) -> Vec<RangeMatch> {
let mut walk = RangeWalk {
source,
destination,
diff,
node_cache,
source_columns: compute_row_byte_lengths(&source.contents),
destination_columns: compute_row_byte_lengths(&destination.contents),
source_text: SourceText::new(&source.contents),
destination_text: SourceText::new(&destination.contents),
source_is_before,
options,
last_non_move_range: TextRange::zero(),
ranges: Vec::new(),
current_range: RangeMatch::zero(),
};
match (&source.ast, &destination.ast) {
(None, None) => {}
(Some(source_tree), None) => {
let source_root = source_tree.root_node();
walk.ranges.push(RangeMatch {
source: TextRange::from_treesitter_range(source_root.range(), &walk.source_columns),
destination: TextRange::zero(),
operation: TextOperation::Delete,
});
}
(None, Some(destination_tree)) => {
let destination_root = destination_tree.root_node();
walk.ranges.push(RangeMatch {
source: TextRange::zero(),
destination: TextRange::from_treesitter_range(
destination_root.range(),
&walk.destination_columns,
),
operation: TextOperation::Insert,
});
}
(Some(source_tree), Some(_destination_tree)) => {
walk.visit_tree(source_tree.root_node());
}
}
walk.ranges
}
impl RangeWalk<'_> {
fn visit_tree(&mut self, root: Node) {
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if !self.visit(node) {
continue;
}
let mut child_cursor = node.walk();
let children: Vec<_> = node.children(&mut child_cursor).collect();
for child in children.into_iter().rev() {
stack.push(child);
}
}
if !self.current_range.is_zero() {
let finished = std::mem::replace(&mut self.current_range, RangeMatch::zero());
self.ranges.push(finished);
}
}
fn visit(&mut self, node: Node) -> bool {
let Some((mapped_id, mapping)) = self.diff.mapping_for_node(&node.id()) else {
return true;
};
let change = classify_node(
node,
mapped_id,
&mapping.operation,
self.node_cache,
self.source.contents.as_bytes(),
self.destination.contents.as_bytes(),
);
let (new_ranges, descend) = match change {
NodeChange::Identical(Some(destination_node)) if self.in_group(node) => {
(vec![self.group_member_move(node, destination_node)], false)
}
NodeChange::Identical(Some(destination_node)) => (
vec![self.identical_or_move(node, destination_node, mapping.reason)],
false,
),
NodeChange::Identical(None) => (Vec::new(), true),
NodeChange::PrunedSubtree(operation) => (vec![self.placed(node, operation)], false),
NodeChange::Leaf(operation) => (vec![self.placed(node, operation)], true),
NodeChange::Update(Some(destination_node)) => {
(self.update_ranges(node, destination_node), true)
}
NodeChange::Update(None) => (vec![self.placed(node, TextOperation::Update)], true),
NodeChange::OwnContentChanged(destination_node) => (
self.own_content_update_ranges(node, destination_node),
false,
),
NodeChange::Descend => match self.whole_content_prune(node, &mapping.operation) {
Some(operation) => (self.own_gap_ranges(node, operation), false),
None => (Vec::new(), true),
},
};
self.push(new_ranges);
descend
}
fn in_group(&self, node: Node) -> bool {
if self.source_is_before {
self.diff.before_group(node.id()).is_some()
} else {
self.diff.after_group(node.id()).is_some()
}
}
fn group_member_move(&self, node: Node, destination_node: Node) -> RangeMatch {
RangeMatch {
source: TextRange::from_treesitter_range(node.range(), &self.source_columns),
destination: TextRange::from_treesitter_range(
destination_node.range(),
&self.destination_columns,
),
operation: TextOperation::Move,
}
}
fn placed(&mut self, node: Node, operation: TextOperation) -> RangeMatch {
advance_and_build_range(
&mut self.last_non_move_range,
node,
&self.source_columns,
operation,
)
}
fn push(&mut self, new_ranges: Vec<RangeMatch>) {
if new_ranges.len() > 1 {
if !self.current_range.is_zero() {
let finished = std::mem::replace(&mut self.current_range, RangeMatch::zero());
self.ranges.push(finished);
}
self.ranges.extend(new_ranges);
return;
}
for new_range in new_ranges {
if new_range.extends(
&self.current_range,
&self.source_text,
&self.destination_text,
) {
self.current_range.extend_into(&new_range);
} else {
if !self.current_range.is_zero() {
let finished = std::mem::replace(&mut self.current_range, RangeMatch::zero());
self.ranges.push(finished);
}
self.current_range = new_range;
}
}
}
fn identical_or_move(
&mut self,
node: Node,
destination_node: Node,
reason: ASTMappingReason,
) -> RangeMatch {
let s = TextRange::from_treesitter_range(node.range(), &self.source_columns);
let d =
TextRange::from_treesitter_range(destination_node.range(), &self.destination_columns);
let crossed_backwards = s.end_row > s.start_row
&& (d.start_row, d.start_column)
< (
self.last_non_move_range.start_row,
self.last_non_move_range.start_column,
);
let displaced_beside_an_edit_on_its_first_row = !self.options.paint_displaced_moves
&& d.end_row as i64 - s.end_row as i64 == d.start_row as i64 - s.start_row as i64
&& s.end_column == d.end_column
&& node_first_row_tail_untouched(
&self.source.contents,
&self.destination.contents,
&s,
&d,
);
let shifted_within_its_own_line = s.end_row > s.start_row
&& (s.start_row == d.start_row || displaced_beside_an_edit_on_its_first_row);
let shifted_by_an_edit_beside_it = s.end_row == s.start_row
&& (s.start_row == d.start_row || !self.options.paint_displaced_moves)
&& node_untouched_on_its_row(
&self.source.contents,
&self.destination.contents,
&s,
&d,
!self.options.paint_displaced_moves,
);
let column_shift_is_meaningful = s.start_column != d.start_column
&& !shifted_within_its_own_line
&& !shifted_by_an_edit_beside_it;
let known_pure_reindent = !self.options.paint_reindent_only_moves
&& matches!(
reason,
ASTMappingReason::NestedConditionCollapse | ASTMappingReason::WrapGrowth
);
let known_pure_relocation = reason == ASTMappingReason::HeritageClauseGrowth;
let operation = if (!column_shift_is_meaningful && !crossed_backwards)
|| known_pure_reindent
|| known_pure_relocation
{
self.last_non_move_range = d.clone();
TextOperation::Identical
} else {
TextOperation::Move
};
RangeMatch {
source: s,
destination: d,
operation,
}
}
fn update_ranges(&mut self, node: Node, destination_node: Node) -> Vec<RangeMatch> {
let source_text = node
.utf8_text(self.source.contents.as_bytes())
.unwrap_or("");
let destination_text = destination_node
.utf8_text(self.destination.contents.as_bytes())
.unwrap_or("");
intra_node_update_ranges(
&mut self.last_non_move_range,
TextRange::from_treesitter_range(node.range(), &self.source_columns),
TextSpan {
text: source_text,
start: node.start_position(),
columns: &self.source_columns,
},
TextSpan {
text: destination_text,
start: destination_node.start_position(),
columns: &self.destination_columns,
},
self.source_is_before,
is_content_node(node.kind()),
self.options.whole_pair_updates
|| splits_a_whole_token(source_text, destination_text)
|| (self.options.whole_identifier_updates
&& is_renamed_identifier(node.kind(), source_text, destination_text)),
)
}
fn own_content_update_ranges(&mut self, node: Node, destination_node: Node) -> Vec<RangeMatch> {
match (own_content_span(node), own_content_span(destination_node)) {
(Some((s_start, s_from, s_to)), Some((d_start, d_from, d_to))) => {
intra_node_update_ranges(
&mut self.last_non_move_range,
TextRange::from_treesitter_range(node.range(), &self.source_columns),
TextSpan {
text: &self.source.contents[s_from..s_to],
start: s_start,
columns: &self.source_columns,
},
TextSpan {
text: &self.destination.contents[d_from..d_to],
start: d_start,
columns: &self.destination_columns,
},
self.source_is_before,
is_content_node(node.kind()),
self.options.whole_pair_updates
|| splits_a_whole_token(
&self.source.contents[s_from..s_to],
&self.destination.contents[d_from..d_to],
)
|| (self.options.whole_identifier_updates
&& is_renamed_identifier(
node.kind(),
&self.source.contents[s_from..s_to],
&self.destination.contents[d_from..d_to],
)),
)
}
_ => vec![self.placed(node, TextOperation::Update)],
}
}
fn whole_content_prune(
&self,
node: Node,
operation: &ASTMappingOperation,
) -> Option<TextOperation> {
let operation = match operation {
ASTMappingOperation::Insert => TextOperation::Insert,
ASTMappingOperation::Delete => TextOperation::Delete,
_ => return None,
};
let fires = node.child_count() > 0
&& is_content_node(node.kind())
&& node
.children(&mut node.walk())
.all(|c| c.child_count() == 0)
&& own_content_span(node)
.is_some_and(|(_, from, to)| !self.source.contents[from..to].trim().is_empty());
fires.then_some(operation)
}
fn own_gap_ranges(&mut self, node: Node, operation: TextOperation) -> Vec<RangeMatch> {
let mut new_ranges = Vec::new();
let mut pos = node.start_byte();
let mut gap_start_point = node.start_position();
let mut child_cursor = node.walk();
for child in node.children(&mut child_cursor) {
if child.start_byte() > pos {
new_ranges.extend(self.gap_range(
pos,
child.start_byte(),
gap_start_point,
&operation,
));
}
new_ranges.push(self.placed(child, operation.clone()));
pos = pos.max(child.end_byte());
gap_start_point = child.end_position();
}
if node.end_byte() > pos {
new_ranges.extend(self.gap_range(pos, node.end_byte(), gap_start_point, &operation));
}
new_ranges
}
fn gap_range(
&mut self,
from: usize,
to: usize,
start: Point,
operation: &TextOperation,
) -> Option<RangeMatch> {
let gap_text = &self.source.contents[from..to];
if gap_text.trim().is_empty() {
return None;
}
let gap_end = point_at_byte_offset(gap_text, start, gap_text.len());
Some(advance_and_build_range_with_source(
&mut self.last_non_move_range,
text_range_from_points(start, gap_end, &self.source_columns),
operation.clone(),
))
}
}
fn node_untouched_on_its_row(
source: &str,
destination: &str,
s: &TextRange,
d: &TextRange,
read_rows_carrying_several_edits: bool,
) -> bool {
let row = |text: &str, row: usize| -> Vec<char> {
text.split('\n')
.nth(row)
.map(|line| line.chars().collect())
.unwrap_or_default()
};
let source_row = row(source, s.start_row);
let destination_row = row(destination, d.start_row);
let prefix = source_row
.iter()
.zip(destination_row.iter())
.take_while(|(a, b)| a == b)
.count();
let suffix = source_row
.iter()
.rev()
.zip(destination_row.iter().rev())
.take_while(|(a, b)| a == b)
.count()
.min(source_row.len() - prefix)
.min(destination_row.len() - prefix);
let in_prefix = s.end_column <= prefix && d.end_column <= prefix;
let in_suffix = s.start_column + suffix >= source_row.len()
&& d.start_column + suffix >= destination_row.len();
in_prefix
|| in_suffix
|| (read_rows_carrying_several_edits
&& s.start_row == d.start_row
&& row_was_edited_rather_than_rewritten(&source_row, &destination_row, prefix, suffix)
&& node_uniquely_placed_on_its_row(&source_row, &destination_row, s, d))
}
fn row_was_edited_rather_than_rewritten(
source_row: &[char],
destination_row: &[char],
prefix: usize,
suffix: usize,
) -> bool {
let indentation = |row: &[char]| row.iter().take_while(|c| c.is_whitespace()).count();
suffix > 0 && prefix > indentation(source_row).max(indentation(destination_row))
}
const MAX_ROW_FOR_UNIQUENESS_SCAN: usize = 2_000;
fn node_uniquely_placed_on_its_row(
source_row: &[char],
destination_row: &[char],
s: &TextRange,
d: &TextRange,
) -> bool {
if source_row.len() > MAX_ROW_FOR_UNIQUENESS_SCAN
|| destination_row.len() > MAX_ROW_FOR_UNIQUENESS_SCAN
{
return false;
}
let (Some(text), Some(other)) = (
source_row.get(s.start_column..s.end_column),
destination_row.get(d.start_column..d.end_column),
) else {
return false;
};
if text.is_empty() || text != other {
return false;
}
fn only_occurrence_of(row: &[char], text: &[char]) -> Option<usize> {
let mut found = None;
for (at, window) in row.windows(text.len()).enumerate() {
if window == text {
if found.is_some() {
return None;
}
found = Some(at);
}
}
found
}
only_occurrence_of(source_row, text) == Some(s.start_column)
&& only_occurrence_of(destination_row, text) == Some(d.start_column)
}
fn node_first_row_tail_untouched(
source: &str,
destination: &str,
s: &TextRange,
d: &TextRange,
) -> bool {
let tail = |text: &str, row: usize, column: usize| -> Option<Vec<char>> {
let line: Vec<char> = text.split('\n').nth(row)?.chars().collect();
(column <= line.len()).then(|| line[column..].to_vec())
};
match (
tail(source, s.start_row, s.start_column),
tail(destination, d.start_row, d.start_column),
) {
(Some(source_tail), Some(destination_tail)) => source_tail == destination_tail,
_ => false,
}
}
fn advance_and_build_range(
last_non_move_range: &mut TextRange,
node: Node,
columns: &[usize],
operation: TextOperation,
) -> RangeMatch {
advance_and_build_range_with_source(
last_non_move_range,
TextRange::from_treesitter_range(node.range(), columns),
operation,
)
}
fn advance_and_build_range_with_source(
last_non_move_range: &mut TextRange,
source_range: TextRange,
operation: TextOperation,
) -> RangeMatch {
*last_non_move_range = last_non_move_range.right_limit();
RangeMatch {
source: source_range,
destination: last_non_move_range.clone(),
operation,
}
}
fn merge_ranges(
source_ranges: &[RangeMatch],
destination_ranges: &[RangeMatch],
) -> Vec<RangeMatch> {
let mut result = Vec::new();
let mut i = 0;
let mut j = 0;
while i < source_ranges.len() {
while j < destination_ranges.len()
&& destination_ranges[j].operation == TextOperation::Insert
{
result.push(RangeMatch {
source: destination_ranges[j].destination.clone(),
destination: destination_ranges[j].source.clone(),
operation: TextOperation::Delete,
});
j += 1;
}
result.push(source_ranges[i].clone());
i += 1;
j += 1;
}
result
}
fn rows_covered(range: &TextRange) -> usize {
let end_row = if range.end_column == 0 {
range.end_row
} else {
range.end_row + 1
};
end_row.saturating_sub(range.start_row).max(1)
}
fn range_contains(outer: &TextRange, inner: &TextRange) -> bool {
(outer.start_row, outer.start_column) <= (inner.start_row, inner.start_column)
&& (inner.end_row, inner.end_column) <= (outer.end_row, outer.end_column)
}
fn reconcile_moves(before: &mut [RangeMatch], after: &mut [RangeMatch], options: RenderOptions) {
use std::collections::HashMap;
let key = |r: &TextRange| (r.start_row, r.start_column, r.end_row, r.end_column);
let index_of = |ranges: &[RangeMatch]| -> HashMap<(usize, usize, usize, usize), usize> {
let mut map = HashMap::new();
for (index, range_match) in ranges.iter().enumerate() {
if range_match.source.is_empty() {
continue;
}
map.entry(key(&range_match.source)).or_insert(index);
}
map
};
let before_index = index_of(before);
let after_index = index_of(after);
let disagreements = |side: &[RangeMatch],
other: &[RangeMatch],
other_index: &HashMap<(usize, usize, usize, usize), usize>|
-> Vec<(usize, Option<usize>)> {
side.iter()
.enumerate()
.filter(|(_, range_match)| {
range_match.operation == TextOperation::Move && !range_match.destination.is_empty()
})
.filter_map(|(index, range_match)| {
match other_index.get(&key(&range_match.destination)).copied() {
Some(other_index) if other[other_index].operation == TextOperation::Move => {
None
}
None if options.paint_resized_moves
&& other.iter().any(|other| {
other.operation == TextOperation::Move
&& !other.source.is_empty()
&& (range_contains(&range_match.destination, &other.source)
|| range_contains(&other.source, &range_match.destination))
}) =>
{
None
}
counterpart => Some((index, counterpart)),
}
})
.collect()
};
let before_disagreed = disagreements(before, after, &after_index);
let after_disagreed = disagreements(after, before, &before_index);
if before_disagreed.is_empty() && after_disagreed.is_empty() {
return;
}
let blamed_rows = |ranges: &[RangeMatch], disagreed: &[(usize, Option<usize>)]| -> usize {
disagreed
.iter()
.map(|&(index, _)| rows_covered(&ranges[index].source))
.sum()
};
let before_rows = blamed_rows(before, &before_disagreed);
let after_rows = blamed_rows(after, &after_disagreed);
let (winner, loser) = if before_rows <= after_rows {
(&before_disagreed, &mut *after)
} else {
(&after_disagreed, &mut *before)
};
for &(index, _) in if before_rows <= after_rows {
&after_disagreed
} else {
&before_disagreed
} {
loser[index].operation = TextOperation::Identical;
}
for &(_, counterpart) in winner {
if let Some(counterpart) = counterpart {
loser[counterpart].operation = TextOperation::Move;
}
}
}
impl TextDiff {
pub fn from(before: &Code, after: &Code, diff: &ASTDiff, node_cache: &NodeCache) -> Self {
Self::from_with_options(before, after, diff, node_cache, RenderOptions::FULL)
}
pub fn from_with_options(
before: &Code,
after: &Code,
diff: &ASTDiff,
node_cache: &NodeCache,
options: RenderOptions,
) -> Self {
let mut before_ranges_plain = ranges(before, after, diff, node_cache, true, options);
let mut after_ranges_plain = ranges(after, before, diff, node_cache, false, options);
reconcile_moves(&mut before_ranges_plain, &mut after_ranges_plain, options);
let before_ranges = merge_ranges(&before_ranges_plain, &after_ranges_plain);
let after_ranges = merge_ranges(&after_ranges_plain, &before_ranges_plain);
Self {
before_ranges,
after_ranges,
}
}
pub fn all(&self, side: usize) -> Vec<RangeMatch> {
if side == 0 {
return self.before_ranges.clone();
}
self.after_ranges.clone()
}
}
#[cfg(test)]
mod tests;