use std::ops::{Deref, Range};
use std::sync::Arc;
use super::line_index::{LineIndex, LineStarts};
#[derive(Debug, Clone, Default, Eq)]
pub struct TextBuffer {
text: Arc<str>,
line_starts: LineStarts,
}
impl PartialEq for TextBuffer {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.text, &other.text) || self.text == other.text
}
}
impl TextBuffer {
pub fn new(text: impl Into<Arc<str>>) -> Self {
let text = text.into();
let line_starts = LineStarts::new(&text);
Self { text, line_starts }
}
pub fn text(&self) -> &str {
&self.text
}
pub fn text_arc(&self) -> Arc<str> {
Arc::clone(&self.text)
}
pub fn line_starts(&self) -> &LineStarts {
&self.line_starts
}
pub fn line_index(&self) -> LineIndex<'_> {
LineIndex::with_starts(&self.text, &self.line_starts)
}
pub fn replace_range(&mut self, range: Range<usize>, insert: &str) {
let removed = self.text[range.clone()].len();
self.line_starts.patch(range.clone(), insert);
let old = &self.text;
let mut new = String::with_capacity(old.len() - removed + insert.len());
new.push_str(&old[..range.start]);
new.push_str(insert);
new.push_str(&old[range.end..]);
self.text = Arc::from(new);
self.debug_assert_in_step();
}
pub fn set_text(&mut self, text: impl Into<Arc<str>>) {
self.text = text.into();
self.line_starts = LineStarts::new(&self.text);
}
pub fn into_string(self) -> String {
self.text.to_string()
}
fn debug_assert_in_step(&self) {
debug_assert!(
self.line_starts == LineStarts::new(&self.text),
"line table drifted from the buffer"
);
}
}
impl Deref for TextBuffer {
type Target = str;
fn deref(&self) -> &str {
&self.text
}
}
impl PartialEq<str> for TextBuffer {
fn eq(&self, other: &str) -> bool {
&*self.text == other
}
}
impl PartialEq<TextBuffer> for str {
fn eq(&self, other: &TextBuffer) -> bool {
self == &*other.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)
}
}
impl From<Arc<str>> for TextBuffer {
fn from(text: Arc<str>) -> Self {
Self::new(text)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_empty_buffer_has_one_line() {
let buffer = TextBuffer::default();
assert_eq!(buffer.line_index().line_count(), 1);
assert_eq!(&*buffer, "");
}
#[test]
fn edits_keep_the_table_in_step() {
let mut buffer = TextBuffer::from("ab\ncd\nef");
buffer.replace_range(2..2, "\nxy");
assert_eq!(&*buffer, "ab\nxy\ncd\nef");
assert_eq!(buffer.line_starts(), &LineStarts::new(&buffer));
buffer.replace_range(2..9, "");
assert_eq!(&*buffer, "abef");
assert_eq!(buffer.line_starts(), &LineStarts::new(&buffer));
assert_eq!(buffer.line_index().line_count(), 1);
}
#[test]
fn set_text_rescans() {
let mut buffer = TextBuffer::from("one line");
buffer.set_text("two\nlines".to_string());
assert_eq!(buffer.line_index().line_count(), 2);
assert_eq!(buffer.line_starts(), &LineStarts::new(&buffer));
}
#[test]
fn text_arc_shares_until_an_edit_and_then_snapshots() {
let mut buffer = TextBuffer::from("ab\ncd");
let before = buffer.text_arc();
assert!(Arc::ptr_eq(&before, &buffer.text_arc()));
assert!(Arc::ptr_eq(&before, &buffer.clone().text_arc()));
buffer.replace_range(2..2, "\nxy");
assert!(
!Arc::ptr_eq(&before, &buffer.text_arc()),
"an edit must not mutate a shared allocation"
);
assert_eq!(&*before, "ab\ncd");
assert_eq!(buffer.text(), "ab\nxy\ncd");
}
#[test]
#[should_panic(expected = "byte range starts at 4 but ends at 2")]
#[expect(
clippy::reversed_empty_ranges,
reason = "the malformed range is the subject of the test"
)]
fn a_reversed_edit_range_panics() {
TextBuffer::from("abcdefgh").replace_range(4..2, "Z");
}
#[test]
#[should_panic(expected = "out of bounds")]
fn an_out_of_bounds_edit_range_panics() {
TextBuffer::from("abcdefgh").replace_range(0..99, "Z");
}
#[test]
#[should_panic(expected = "not a char boundary")]
fn an_edit_range_off_a_char_boundary_panics() {
TextBuffer::from("\u{1F600}x").replace_range(1..2, "Z");
}
}