enum HunkOp<'a> {
Context(&'a str),
Delete(&'a str),
Insert(&'a str),
}
struct Hunk<'a> {
old_start: usize,
old_count: usize,
ops: Vec<HunkOp<'a>>,
}
impl<'a> Hunk<'a> {
fn old_side(&self) -> Vec<&'a str> {
self.ops
.iter()
.filter_map(|op| match op {
HunkOp::Context(t) | HunkOp::Delete(t) => Some(*t),
HunkOp::Insert(_) => None,
})
.collect()
}
fn inserts(&self) -> usize {
self.ops
.iter()
.filter(|op| matches!(op, HunkOp::Insert(_)))
.count()
}
fn trim_padding(&mut self) {
while self.old_side().len() > self.old_count
&& matches!(self.ops.last(), Some(HunkOp::Context("")))
{
self.ops.pop();
}
}
}
fn apply_unified(original: &str, patch: &str) -> Result<(String, usize)> {
let hunks = parse_unified_hunks(patch)?;
if hunks.is_empty() {
return Err(AtomwriteError::InvalidInput {
reason: "no `@@ -a,b +c,d @@` hunk found in unified patch".into(),
}
.into());
}
if let Some(empty) = hunks.iter().position(|h| h.old_side().is_empty() && h.inserts() == 0) {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"unified hunk {}/{} (@@ -{},{} @@) has neither context, removed nor added lines; \
refusing a no-op hunk",
empty + 1,
hunks.len(),
hunks[empty].old_start,
hunks[empty].old_count
),
}
.into());
}
let total = hunks.len();
let mut lines: Vec<&str> = original.lines().collect();
let mut drift: i64 = 0;
for (index, hunk) in hunks.iter().enumerate() {
let old_side = hunk.old_side();
let base = if old_side.is_empty() {
hunk.old_start
} else {
hunk.old_start.saturating_sub(1)
};
let expected = (base as i64 + drift).clamp(0, lines.len() as i64) as usize;
let Some(pos) = locate_hunk(&lines, &old_side, expected) else {
return Err(hunk_mismatch(index + 1, total, hunk, &lines, expected, &old_side).into());
};
let mut new_lines: Vec<&str> = Vec::with_capacity(hunk.ops.len());
let mut cursor = pos;
for op in &hunk.ops {
match op {
HunkOp::Context(_) => {
new_lines.push(lines[cursor]);
cursor += 1;
}
HunkOp::Delete(_) => cursor += 1,
HunkOp::Insert(text) => new_lines.push(text),
}
}
let new_len = new_lines.len() as i64;
lines.splice(pos..pos + old_side.len(), new_lines);
drift += new_len - old_side.len() as i64 + (pos as i64 - expected as i64);
}
let mut result = lines.join("\n");
if original.ends_with('\n') && !result.ends_with('\n') {
result.push('\n');
}
Ok((result, total))
}
fn locate_hunk(lines: &[&str], old_side: &[&str], expected: usize) -> Option<usize> {
if old_side.is_empty() {
return Some(expected.min(lines.len()));
}
let fits = |pos: usize| {
pos + old_side.len() <= lines.len()
&& old_side
.iter()
.zip(&lines[pos..])
.all(|(want, have)| want.trim_end() == have.trim_end())
};
if fits(expected) {
return Some(expected);
}
for shift in 1..=crate::constants::PATCH_MAX_HUNK_SHIFT {
if let Some(earlier) = expected.checked_sub(shift)
&& fits(earlier)
{
return Some(earlier);
}
if fits(expected + shift) {
return Some(expected + shift);
}
}
None
}
fn hunk_mismatch(
number: usize,
total: usize,
hunk: &Hunk<'_>,
lines: &[&str],
expected: usize,
old_side: &[&str],
) -> AtomwriteError {
let at = (0..old_side.len())
.find(|k| {
lines
.get(expected + k)
.is_none_or(|have| have.trim_end() != old_side[*k].trim_end())
})
.unwrap_or(0);
let line_no = expected + at + 1;
let want = clip(old_side.get(at).copied().unwrap_or(""));
let have = lines.get(expected + at).map(|l| clip(l));
let found = have
.as_deref()
.map_or_else(|| "<end of file>".to_owned(), |l| format!("{l:?}"));
AtomwriteError::MatchFailed {
reason: format!(
"unified hunk {number}/{total} (@@ -{}) does not match the file: line {line_no} \
expected {want:?}, found {found}; nothing was written",
hunk.old_start
),
best_candidate: Some(Box::new(crate::ndjson_types::BestCandidate {
text: have,
line: Some(line_no as u64),
column: Some(1),
similarity: None,
strategy: Some("unified-hunk".to_owned()),
diff_preview: None,
})),
candidates: None,
}
}
fn clip(line: &str) -> String {
line.chars()
.take(crate::constants::PATCH_ERROR_SNIPPET_CHARS)
.collect()
}
fn parse_unified_hunks(patch: &str) -> Result<Vec<Hunk<'_>>> {
let mut hunks = Vec::with_capacity(crate::constants::PATCH_HUNKS_CAPACITY);
let mut current: Option<Hunk<'_>> = None;
for (number, line) in patch.lines().enumerate() {
if let Some(header) = line.strip_prefix("@@ ") {
if let Some(mut done) = current.take() {
done.trim_padding();
hunks.push(done);
}
let Some((old_start, old_count)) = parse_hunk_header(header) else {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"invalid hunk header at patch line {}: {:?}",
number + 1,
clip(line)
),
}
.into());
};
current = Some(Hunk {
old_start,
old_count,
ops: Vec::with_capacity(crate::constants::PATCH_HUNK_OPS_CAPACITY),
});
} else if let Some(ref mut h) = current {
if let Some(text) = line.strip_prefix('+') {
h.ops.push(HunkOp::Insert(text));
} else if let Some(text) = line.strip_prefix('-') {
h.ops.push(HunkOp::Delete(text));
} else if let Some(text) = line.strip_prefix(' ') {
h.ops.push(HunkOp::Context(text));
} else if !line.starts_with('\\') {
h.ops.push(HunkOp::Context(line));
}
}
}
if let Some(mut done) = current {
done.trim_padding();
hunks.push(done);
}
Ok(hunks)
}
fn parse_hunk_header(header: &str) -> Option<(usize, usize)> {
let parts: Vec<&str> = header.split_whitespace().collect();
if parts.len() < crate::constants::PATCH_HUNK_HEADER_MIN_FIELDS {
return None;
}
let old_part = parts[0].strip_prefix('-')?;
parts[1].strip_prefix('+')?;
parse_range(old_part)
}
fn parse_range(range: &str) -> Option<(usize, usize)> {
if let Some((start, count)) = range.split_once(',') {
Some((start.parse().ok()?, count.parse().ok()?))
} else {
Some((range.parse().ok()?, 1))
}
}