use super::{Document, RepairScope};
use crate::error::{Error, Result};
use crate::prelude::*;
#[derive(Debug)]
struct PlannedEdit {
start: usize,
end: usize,
replacement: String,
}
#[derive(Debug)]
#[must_use = "dropping an EditSession aborts its queued edits"]
pub struct EditSession<'a> {
document: &'a mut Document,
edits: Vec<PlannedEdit>,
}
impl<'a> EditSession<'a> {
pub(super) fn new(document: &'a mut Document) -> Self {
Self {
document,
edits: Vec::new(),
}
}
#[must_use]
pub fn source(&self) -> &str {
self.document.source()
}
#[must_use]
pub fn len(&self) -> usize {
self.edits.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.edits.is_empty()
}
pub fn replace_span(
&mut self,
start: usize,
end: usize,
replacement: impl Into<String>,
) -> Result<()> {
let source = self.source();
if start > end || end > source.len() {
return Err(Error::Parse(format!(
"edit-session range {start}..{end} out of bounds (source length {})",
source.len()
)));
}
if !source.is_char_boundary(start) || !source.is_char_boundary(end) {
return Err(Error::Parse(format!(
"edit-session range {start}..{end} is not a character boundary"
)));
}
if self
.edits
.iter()
.any(|queued| queued.start == start || (start < queued.end && queued.start < end))
{
return Err(Error::Parse(format!(
"edit-session range {start}..{end} conflicts with a queued edit"
)));
}
self.edits.push(PlannedEdit {
start,
end,
replacement: replacement.into(),
});
Ok(())
}
pub fn commit(mut self) -> Result<()> {
if self.edits.is_empty() {
return Ok(());
}
self.edits.sort_unstable_by_key(|edit| edit.start);
let source = self.document.source();
let final_len = self.edits.iter().try_fold(source.len(), |length, edit| {
length
.checked_sub(edit.end - edit.start)
.and_then(|length| length.checked_add(edit.replacement.len()))
});
let Some(final_len) = final_len else {
return Err(Error::Parse(
"edit-session candidate length exceeds addressable memory".into(),
));
};
let mut candidate = String::with_capacity(final_len);
let mut cursor = 0;
for edit in &self.edits {
candidate.push_str(&source[cursor..edit.start]);
candidate.push_str(&edit.replacement);
cursor = edit.end;
}
candidate.push_str(&source[cursor..]);
self.document
.commit_source(&candidate, RepairScope::Document)
}
pub fn abort(self) {}
}