use std::ops::Range;
use crate::text::LineIndex;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextBuffer {
text: String,
index: LineIndex,
}
impl TextBuffer {
pub fn new(text: String) -> Self {
let index = LineIndex::new(&text);
Self { text, index }
}
pub fn text(&self) -> &str {
&self.text
}
pub fn line_index(&self) -> &LineIndex {
&self.index
}
pub fn len(&self) -> usize {
self.text.len()
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn apply_edit(&mut self, range: Range<usize>, insert: &str) {
let mut start = range.start.min(self.text.len());
let mut end = range.end.clamp(start, self.text.len());
while !self.text.is_char_boundary(start) {
start -= 1;
}
while !self.text.is_char_boundary(end) {
end += 1;
}
self.text.replace_range(start..end, insert);
self.index.apply_edit(start..end, insert);
debug_assert_eq!(self.index, LineIndex::new(&self.text));
}
}
impl From<String> for TextBuffer {
fn from(text: String) -> Self {
Self::new(text)
}
}
impl From<&str> for TextBuffer {
fn from(text: &str) -> Self {
Self::new(text.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apply_edit_keeps_text_and_index_in_sync() {
let mut buf = TextBuffer::from("ab\ncd\nef");
buf.apply_edit(3..5, "XYZ\nW");
assert_eq!(buf.text(), "ab\nXYZ\nW\nef");
assert_eq!(buf, TextBuffer::from("ab\nXYZ\nW\nef"));
}
#[test]
fn an_inverted_range_is_coerced_rather_than_panicking() {
let mut buf = TextBuffer::from("ab\ncd");
let (start, end) = (4, 1);
buf.apply_edit(start..end, "X");
assert_eq!(buf.text(), "ab\ncXd");
assert_eq!(buf, TextBuffer::from("ab\ncXd"));
}
#[test]
fn a_range_past_the_end_clamps() {
let mut buf = TextBuffer::from("ab");
buf.apply_edit(1..99, "X");
assert_eq!(buf.text(), "aX");
assert_eq!(buf, TextBuffer::from("aX"));
}
#[test]
fn an_offset_inside_a_wide_char_snaps_to_a_boundary() {
let mut buf = TextBuffer::from("a\u{1F600}b");
buf.apply_edit(2..3, "");
assert_eq!(buf.text(), "ab");
assert_eq!(buf, TextBuffer::from("ab"));
}
#[test]
fn a_long_edit_script_stays_in_sync() {
let mut buf = TextBuffer::from("x <- 1\ny <- \"\u{00e1}\"\nz <- \"\u{1F600}\"\n");
let inserts = [
"",
"q",
"\n",
"\nq",
"q\n",
"\u{00e1}",
"\u{1F600}",
"a\nb\nc",
"\r\n",
];
let mut seed: u64 = 0x2545_F491_4F6C_DD1D;
let mut next = move || {
seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
(seed >> 33) as usize
};
for step in 0..500 {
let len = buf.len();
let mut start = if len == 0 { 0 } else { next() % (len + 1) };
let mut end = if len == 0 { 0 } else { next() % (len + 1) };
if start > end {
std::mem::swap(&mut start, &mut end);
}
let insert = inserts[next() % inserts.len()];
buf.apply_edit(start..end, insert);
assert_eq!(
buf.line_index(),
&LineIndex::new(buf.text()),
"step {step}: {start}..{end} insert {insert:?} text {:?}",
buf.text()
);
}
}
}