use super::parser::UpdateFileChunk;
use super::seek::seek_sequence;
type Replacement = (usize, usize, Vec<String>);
pub(crate) fn derive_new_contents(
original: &str,
path: &str,
chunks: &[UpdateFileChunk],
) -> Result<String, String> {
let mut original_lines: Vec<String> = original.split('\n').map(String::from).collect();
if original_lines.last().is_some_and(String::is_empty) {
original_lines.pop();
}
let replacements = compute_replacements(&original_lines, path, chunks)?;
let mut new_lines = apply_replacements(original_lines, &replacements);
if !new_lines.last().is_some_and(String::is_empty) {
new_lines.push(String::new());
}
Ok(new_lines.join("\n"))
}
fn compute_replacements(
original_lines: &[String],
path: &str,
chunks: &[UpdateFileChunk],
) -> Result<Vec<Replacement>, String> {
let mut replacements: Vec<Replacement> = Vec::new();
let mut line_index: usize = 0;
for chunk in chunks {
if let Some(ctx_line) = &chunk.change_context {
if let Some(idx) = seek_sequence(
original_lines,
std::slice::from_ref(ctx_line),
line_index,
false,
) {
line_index = idx + 1;
} else {
return Err(format!("Failed to find context '{ctx_line}' in {path}"));
}
}
if chunk.old_lines.is_empty() {
let insertion_idx = if original_lines.last().is_some_and(String::is_empty) {
original_lines.len() - 1
} else {
original_lines.len()
};
replacements.push((insertion_idx, 0, chunk.new_lines.clone()));
continue;
}
let mut pattern: &[String] = &chunk.old_lines;
let mut new_slice: &[String] = &chunk.new_lines;
let mut found = seek_sequence(original_lines, pattern, line_index, chunk.is_end_of_file);
if found.is_none() && pattern.last().is_some_and(String::is_empty) {
pattern = &pattern[..pattern.len() - 1];
if new_slice.last().is_some_and(String::is_empty) {
new_slice = &new_slice[..new_slice.len() - 1];
}
found = seek_sequence(original_lines, pattern, line_index, chunk.is_end_of_file);
}
if let Some(start_idx) = found {
replacements.push((start_idx, pattern.len(), new_slice.to_vec()));
line_index = start_idx + pattern.len();
} else {
return Err(format!(
"Failed to find expected lines in {}:\n{}",
path,
chunk.old_lines.join("\n"),
));
}
}
replacements.sort_by_key(|(index, _, _)| *index);
Ok(replacements)
}
fn apply_replacements(mut lines: Vec<String>, replacements: &[Replacement]) -> Vec<String> {
for (start_idx, old_len, new_segment) in replacements.iter().rev() {
let start_idx = *start_idx;
for _ in 0..*old_len {
if start_idx < lines.len() {
lines.remove(start_idx);
}
}
for (offset, new_line) in new_segment.iter().enumerate() {
lines.insert(start_idx + offset, new_line.clone());
}
}
lines
}