use super::super::{
imara_unified_hunk_slices, unified_hunk_body_for_diff_options, unified_hunk_header_counts,
};
use super::{StructuredHunk, StructuredLine, StructuredLineKind, StructuredSegment};
use crate::diff::options::DiffOptions;
#[must_use]
pub(crate) fn line_hunks(
old_content: &str,
new_content: &str,
options: &DiffOptions,
enclosing_function_context: bool,
) -> Vec<StructuredHunk> {
if !options.whitespace.is_active() && old_content == new_content {
return Vec::new();
}
let body = unified_hunk_body_for_diff_options(old_content, new_content, options);
structured_hunks_from_unified_body(&body, old_content, new_content, enclosing_function_context)
}
fn structured_hunks_from_unified_body(
body: &str,
old_content: &str,
new_content: &str,
enclosing_function_context: bool,
) -> Vec<StructuredHunk> {
if body.is_empty() {
return Vec::new();
}
let before_ends_nl = old_content.ends_with('\n');
let after_ends_nl = new_content.ends_with('\n');
let before_count = line_count(old_content);
let after_count = line_count(new_content);
let old_line_bodies = super::old_line_bodies_for_context(old_content);
imara_unified_hunk_slices(body)
.into_iter()
.filter_map(|hunk_str| {
let patch_lines = split_unified_hunk_on_lf(hunk_str);
let header = patch_lines.first()?;
let (old_start, old_lines, new_start, new_lines) = unified_hunk_header_counts(header)?;
let mut old_line = old_start;
let mut new_line = new_start;
let mut structured_lines: Vec<StructuredLine> = Vec::new();
for line in patch_lines.iter().skip(1) {
if *line == "\\ No newline at end of file" {
if let Some(last) = structured_lines.last_mut() {
last.no_newline_at_eof = true;
}
continue;
}
let (kind, prefix_len) = match line.as_bytes().first() {
Some(b' ') => (StructuredLineKind::Context, 1),
Some(b'-') => (StructuredLineKind::Del, 1),
Some(b'+') => (StructuredLineKind::Add, 1),
_ => continue,
};
let text = line[prefix_len..].to_owned();
let (old_idx, new_idx) = match kind {
StructuredLineKind::Context => {
let o = old_line;
let n = new_line;
old_line = old_line.saturating_add(1);
new_line = new_line.saturating_add(1);
(Some(o), Some(n))
}
StructuredLineKind::Del => {
let o = old_line;
old_line = old_line.saturating_add(1);
(Some(o), None)
}
StructuredLineKind::Add => {
let n = new_line;
new_line = new_line.saturating_add(1);
(None, Some(n))
}
};
structured_lines.push(StructuredLine {
kind,
old: old_idx,
new: new_idx,
segments: vec![StructuredSegment {
text,
emphasis: false,
}],
no_newline_at_eof: match kind {
StructuredLineKind::Del => line_missing_eof_newline(
old_idx.map(|n| n.saturating_sub(1)),
before_count,
before_ends_nl,
),
StructuredLineKind::Add => line_missing_eof_newline(
new_idx.map(|n| n.saturating_sub(1)),
after_count,
after_ends_nl,
),
StructuredLineKind::Context => {
line_missing_eof_newline(
old_idx.map(|n| n.saturating_sub(1)),
before_count,
before_ends_nl,
) || line_missing_eof_newline(
new_idx.map(|n| n.saturating_sub(1)),
after_count,
after_ends_nl,
)
}
},
});
}
let context = if enclosing_function_context {
structured_lines
.iter()
.filter(|l| l.kind == StructuredLineKind::Del)
.filter_map(|l| l.old)
.min()
.or_else(|| structured_lines.iter().filter_map(|l| l.old).min())
.and_then(|old_line_no| {
super::enclosing_function_context(&old_line_bodies, old_line_no)
})
} else {
None
};
Some(StructuredHunk {
old_start,
new_start,
old_lines,
new_lines,
context,
lines: structured_lines,
})
})
.collect()
}
fn split_unified_hunk_on_lf(hunk: &str) -> Vec<&str> {
if hunk.is_empty() {
return Vec::new();
}
let mut parts: Vec<&str> = hunk.split('\n').collect();
if hunk.ends_with('\n') && parts.last() == Some(&"") {
parts.pop();
}
parts
}
fn line_count(text: &str) -> usize {
if text.is_empty() {
return 0;
}
let ends_nl = text.ends_with('\n');
let mut parts: usize = text.split('\n').count();
if ends_nl && text.ends_with("\n") {
parts = parts.saturating_sub(1);
}
parts
}
fn line_missing_eof_newline(
index: Option<usize>,
line_count: usize,
file_ends_with_newline: bool,
) -> bool {
if file_ends_with_newline {
return false;
}
index == Some(line_count.saturating_sub(1))
}