use super::*;
#[derive(Debug, Clone)]
pub(crate) struct LineCell {
pub(crate) text: String,
pub(crate) last_line_no: usize,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct SparseBuffer {
pub(crate) known: BTreeMap<usize, LineCell>,
pub(crate) seen_total_lines: Option<usize>,
pub(crate) content_ends_with_newline: bool,
}
impl SparseBuffer {
pub(crate) fn reset_to_full(&mut self, content: &str, total_lines: usize, line_no: usize) {
self.known.clear();
for (i, text) in split_lines(content).into_iter().enumerate() {
self.known.insert(
i + 1,
LineCell {
text,
last_line_no: line_no,
},
);
}
self.content_ends_with_newline = content.ends_with('\n');
let normalized_total = self.normalize_total(total_lines);
self.seen_total_lines = Some(normalized_total.max(self.known.len()));
}
pub(crate) fn normalize_total(&self, total_lines: usize) -> usize {
if self.content_ends_with_newline {
total_lines.saturating_sub(1)
} else {
total_lines
}
}
pub(crate) fn splice(
&mut self,
start_line: usize,
lines: &[String],
total_lines: usize,
line_no: usize,
) {
for (i, text) in lines.iter().enumerate() {
self.known.insert(
start_line + i,
LineCell {
text: text.clone(),
last_line_no: line_no,
},
);
}
let norm_total = self.normalize_total(total_lines);
self.seen_total_lines = Some(norm_total.max(self.seen_total_lines.unwrap_or(0)));
}
pub(crate) fn covered_ranges(&self) -> Vec<(usize, usize)> {
let mut ranges: Vec<(usize, usize)> = Vec::new();
for &k in self.known.keys() {
match ranges.last_mut() {
Some(last) if last.1 + 1 == k => last.1 = k,
_ => ranges.push((k, k)),
}
}
ranges
}
pub(crate) fn known_lines(&self) -> Vec<(usize, String)> {
self.known
.iter()
.map(|(k, c)| (*k, c.text.clone()))
.collect()
}
pub(crate) fn known_lines_with_provenance(&self) -> Vec<(usize, String, usize)> {
self.known
.iter()
.map(|(k, c)| (*k, c.text.clone(), c.last_line_no))
.collect()
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum EditOutcome {
Applied,
UnAnchorable,
}
pub(crate) fn apply_edit(
buf: &mut SparseBuffer,
hunks: &[EditHunk],
structured_patch: &Option<Vec<PatchHunk>>,
line_no: usize,
) -> EditOutcome {
if let Some(patches) = structured_patch {
if !patches.is_empty() {
return apply_structured_patch(buf, patches, line_no);
}
}
apply_string_edit(buf, hunks, line_no)
}
pub(crate) fn apply_structured_patch(
buf: &mut SparseBuffer,
patches: &[PatchHunk],
line_no: usize,
) -> EditOutcome {
let mut applied_any = false;
let max_line = buf.known.keys().copied().max().unwrap_or(0);
let patch_max = patches
.iter()
.map(|h| h.old_start + h.old_lines)
.max()
.unwrap_or(0);
let span = max_line.max(patch_max);
let mut dense: Vec<Option<String>> = vec![None; span + 1]; for (k, c) in &buf.known {
if *k <= span {
dense[*k] = Some(c.text.clone());
}
}
let mut offset: isize = 0;
for h in patches {
let old_region: Vec<String> = h
.lines
.iter()
.filter(|l| l.starts_with('-') || l.starts_with(' '))
.map(|l| l[1.min(l.len())..].to_string())
.collect();
let added: Vec<String> = h
.lines
.iter()
.filter(|l| l.starts_with('+') || l.starts_with(' '))
.map(|l| l[1.min(l.len())..].to_string())
.collect();
let start = (h.old_start as isize + offset).max(1) as usize;
let end = start + h.old_lines; if end > dense.len() {
dense.resize(end, None);
}
let neighbourhood_known = (start.saturating_sub(1)..=end)
.any(|i| dense.get(i).map(Option::is_some).unwrap_or(false));
if !neighbourhood_known {
return EditOutcome::UnAnchorable;
}
let region_known = (start..end).all(|i| dense.get(i).map(Option::is_some).unwrap_or(false));
if h.old_lines > 0 && !region_known {
return EditOutcome::UnAnchorable;
}
if h.old_lines > 0 && old_region.len() == h.old_lines {
let matches = (0..h.old_lines).all(|k| {
dense
.get(start + k)
.and_then(|c| c.as_ref())
.map(|t| t == &old_region[k])
.unwrap_or(false)
});
if !matches {
return EditOutcome::UnAnchorable;
}
}
let tail: Vec<Option<String>> = dense.split_off(end.min(dense.len()));
dense.truncate(start.min(dense.len()));
for a in &added {
dense.push(Some(a.clone()));
}
dense.extend(tail);
offset += h.new_lines as isize - h.old_lines as isize;
applied_any = true;
}
buf.known.clear();
for (i, cell) in dense.iter().enumerate().skip(1) {
if let Some(text) = cell {
buf.known.insert(
i,
LineCell {
text: text.clone(),
last_line_no: line_no,
},
);
}
}
let max_known = buf.known.keys().copied().max().unwrap_or(0);
let prev_total = buf.seen_total_lines.unwrap_or(0) as isize;
let adjusted = (prev_total + offset).max(max_known as isize).max(0) as usize;
buf.seen_total_lines = Some(adjusted);
if applied_any {
EditOutcome::Applied
} else {
EditOutcome::UnAnchorable
}
}
pub(crate) fn apply_string_edit(
buf: &mut SparseBuffer,
hunks: &[EditHunk],
line_no: usize,
) -> EditOutcome {
let ranges = buf.covered_ranges();
let contiguous_from_one = matches!(ranges.first(), Some(&(1, _))) && ranges.len() == 1;
if !contiguous_from_one {
return EditOutcome::UnAnchorable;
}
let mut text = buf
.known
.values()
.map(|c| c.text.as_str())
.collect::<Vec<_>>()
.join("\n");
let mut any = false;
for h in hunks {
if h.old_string.is_empty() || !text.contains(&h.old_string) {
return EditOutcome::UnAnchorable;
}
if h.replace_all {
text = text.replace(&h.old_string, &h.new_string);
} else {
text = text.replacen(&h.old_string, &h.new_string, 1);
}
any = true;
}
if !any {
return EditOutcome::UnAnchorable;
}
buf.known.clear();
for (i, line) in text.split('\n').enumerate() {
buf.known.insert(
i + 1,
LineCell {
text: line.to_string(),
last_line_no: line_no,
},
);
}
let total = buf.known.len();
buf.seen_total_lines = Some(total.max(buf.seen_total_lines.unwrap_or(0)));
EditOutcome::Applied
}