use std::fmt;
const DEFAULT_GAP_SIZE: usize = 64;
const MIN_GAP_GROW: usize = 64;
#[derive(Clone)]
pub struct GapBuffer {
buf: Vec<u8>,
gap_start: usize,
gap_end: usize,
}
impl GapBuffer {
pub fn new() -> Self {
Self {
buf: vec![0u8; DEFAULT_GAP_SIZE],
gap_start: 0,
gap_end: DEFAULT_GAP_SIZE,
}
}
pub fn from_str(s: &str) -> Self {
let text = s.as_bytes();
let gap = DEFAULT_GAP_SIZE;
let mut buf = Vec::with_capacity(text.len() + gap);
buf.extend_from_slice(text);
buf.resize(text.len() + gap, 0);
Self {
buf,
gap_start: text.len(),
gap_end: text.len() + gap,
}
}
#[inline]
pub fn len(&self) -> usize {
self.buf.len() - self.gap_size()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn char_count(&self) -> usize {
let before = std::str::from_utf8(&self.buf[..self.gap_start])
.expect("pre-gap text is not valid UTF-8");
let after = std::str::from_utf8(&self.buf[self.gap_end..])
.expect("post-gap text is not valid UTF-8");
before.chars().count() + after.chars().count()
}
#[inline]
pub fn gap_size(&self) -> usize {
self.gap_end - self.gap_start
}
pub fn byte_at(&self, pos: usize) -> u8 {
assert!(
pos < self.len(),
"byte_at: position {pos} out of range (len {})",
self.len()
);
if pos < self.gap_start {
self.buf[pos]
} else {
self.buf[pos + self.gap_size()]
}
}
pub fn char_at(&self, pos: usize) -> Option<char> {
if pos >= self.len() {
return None;
}
let first = self.byte_at(pos);
let char_len = utf8_char_len(first);
assert!(
pos + char_len <= self.len(),
"char_at: incomplete UTF-8 sequence at byte position {pos}"
);
let mut tmp = [0u8; 4];
for i in 0..char_len {
tmp[i] = self.byte_at(pos + i);
}
let s = std::str::from_utf8(&tmp[..char_len]).expect("char_at: invalid UTF-8 sequence");
s.chars().next()
}
pub fn text_range(&self, start: usize, end: usize) -> String {
assert!(start <= end, "text_range: start ({start}) > end ({end})");
assert!(
end <= self.len(),
"text_range: end ({end}) > len ({})",
self.len()
);
if start == end {
return String::new();
}
let mut out = Vec::with_capacity(end - start);
if start < self.gap_start {
let seg_end = end.min(self.gap_start);
out.extend_from_slice(&self.buf[start..seg_end]);
}
if end > self.gap_start {
let seg_start = start.max(self.gap_start);
let phys_start = seg_start + self.gap_size();
let phys_end = end + self.gap_size();
out.extend_from_slice(&self.buf[phys_start..phys_end]);
}
String::from_utf8(out).expect("text_range: extracted bytes are not valid UTF-8")
}
pub fn copy_bytes_to(&self, start: usize, end: usize, out: &mut Vec<u8>) {
assert!(start <= end, "copy_bytes_to: start ({start}) > end ({end})");
assert!(
end <= self.len(),
"copy_bytes_to: end ({end}) > len ({})",
self.len()
);
out.clear();
if start == end {
return;
}
out.reserve(end - start);
if start < self.gap_start {
let seg_end = end.min(self.gap_start);
out.extend_from_slice(&self.buf[start..seg_end]);
}
if end > self.gap_start {
let seg_start = start.max(self.gap_start);
let phys_start = seg_start + self.gap_size();
let phys_end = end + self.gap_size();
out.extend_from_slice(&self.buf[phys_start..phys_end]);
}
}
pub fn to_string(&self) -> String {
self.text_range(0, self.len())
}
pub fn insert_str(&mut self, pos: usize, s: &str) {
assert!(
pos <= self.len(),
"insert_str: position {pos} out of range (len {})",
self.len()
);
if s.is_empty() {
return;
}
debug_assert!(
pos == self.len() || self.is_char_boundary(pos),
"insert_str: position {pos} is not on a UTF-8 character boundary"
);
let bytes = s.as_bytes();
self.move_gap_to(pos);
self.ensure_gap(bytes.len());
self.buf[self.gap_start..self.gap_start + bytes.len()].copy_from_slice(bytes);
self.gap_start += bytes.len();
}
pub fn delete_range(&mut self, start: usize, end: usize) {
assert!(start <= end, "delete_range: start ({start}) > end ({end})");
assert!(
end <= self.len(),
"delete_range: end ({end}) > len ({})",
self.len()
);
if start == end {
return;
}
debug_assert!(
self.is_char_boundary(start),
"delete_range: start ({start}) is not on a UTF-8 character boundary"
);
debug_assert!(
end == self.len() || self.is_char_boundary(end),
"delete_range: end ({end}) is not on a UTF-8 character boundary"
);
self.move_gap_to(start);
self.gap_end += end - start;
}
pub fn replace_same_len_range(&mut self, start: usize, end: usize, replacement: &str) {
assert!(
start <= end,
"replace_same_len_range: start ({start}) > end ({end})"
);
assert!(
end <= self.len(),
"replace_same_len_range: end ({end}) > len ({})",
self.len()
);
assert_eq!(
replacement.len(),
end - start,
"replace_same_len_range: replacement byte length ({}) must match replaced length ({})",
replacement.len(),
end - start
);
if start == end {
return;
}
debug_assert!(
self.is_char_boundary(start),
"replace_same_len_range: start ({start}) is not on a UTF-8 character boundary"
);
debug_assert!(
end == self.len() || self.is_char_boundary(end),
"replace_same_len_range: end ({end}) is not on a UTF-8 character boundary"
);
self.move_gap_to(end);
self.buf[start..end].copy_from_slice(replacement.as_bytes());
}
pub fn move_gap_to(&mut self, pos: usize) {
assert!(
pos <= self.len(),
"move_gap_to: position {pos} out of range (len {})",
self.len()
);
if pos == self.gap_start {
return;
}
let gap = self.gap_size();
if pos < self.gap_start {
let count = self.gap_start - pos;
self.buf.copy_within(pos..pos + count, pos + gap);
self.gap_start = pos;
self.gap_end = pos + gap;
} else {
let count = pos - self.gap_start;
let src_start = self.gap_end;
let dst_start = self.gap_start;
self.buf
.copy_within(src_start..src_start + count, dst_start);
self.gap_start = pos;
self.gap_end = pos + gap;
}
}
pub fn ensure_gap(&mut self, min_size: usize) {
if self.gap_size() >= min_size {
return;
}
let grow = (min_size - self.gap_size()).max(MIN_GAP_GROW);
let old_gap_end = self.gap_end;
let after_gap_len = self.buf.len() - old_gap_end;
self.buf.resize(self.buf.len() + grow, 0);
if after_gap_len > 0 {
self.buf
.copy_within(old_gap_end..old_gap_end + after_gap_len, old_gap_end + grow);
}
self.gap_end += grow;
}
pub fn byte_to_char(&self, byte_pos: usize) -> usize {
assert!(
byte_pos <= self.len(),
"byte_to_char: byte_pos ({byte_pos}) > len ({})",
self.len()
);
if byte_pos == 0 {
return 0;
}
let mut chars = 0usize;
if byte_pos <= self.gap_start {
let slice = &self.buf[..byte_pos];
let s = std::str::from_utf8(slice)
.expect("byte_to_char: not a valid UTF-8 boundary (segment A)");
chars = s.chars().count();
} else {
let pre = std::str::from_utf8(&self.buf[..self.gap_start])
.expect("byte_to_char: pre-gap segment is not valid UTF-8");
chars += pre.chars().count();
let remaining = byte_pos - self.gap_start;
let post_slice = &self.buf[self.gap_end..self.gap_end + remaining];
let post = std::str::from_utf8(post_slice)
.expect("byte_to_char: not a valid UTF-8 boundary (segment B)");
chars += post.chars().count();
}
chars
}
pub fn char_to_byte(&self, char_pos: usize) -> usize {
if char_pos == 0 {
return 0;
}
let mut remaining = char_pos;
let pre = std::str::from_utf8(&self.buf[..self.gap_start])
.expect("char_to_byte: pre-gap segment is not valid UTF-8");
for (byte_offset, _ch) in pre.char_indices() {
if remaining == 0 {
return byte_offset;
}
remaining -= 1;
}
if remaining == 0 {
return self.gap_start;
}
let post = std::str::from_utf8(&self.buf[self.gap_end..])
.expect("char_to_byte: post-gap segment is not valid UTF-8");
for (byte_offset, _ch) in post.char_indices() {
if remaining == 0 {
return self.gap_start + byte_offset;
}
remaining -= 1;
}
if remaining == 0 {
return self.len();
}
tracing::debug!(
"char_to_byte: char_pos ({char_pos}) exceeds char_count ({}), clamping",
self.char_count()
);
return self.len();
}
fn is_char_boundary(&self, pos: usize) -> bool {
if pos == 0 || pos >= self.len() {
return true;
}
let b = self.byte_at(pos);
is_utf8_start_byte(b)
}
pub(crate) fn dump_text(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(self.len());
out.extend_from_slice(&self.buf[..self.gap_start]);
out.extend_from_slice(&self.buf[self.gap_end..]);
out
}
pub(crate) fn from_dump(text: Vec<u8>) -> Self {
let len = text.len();
Self {
buf: text,
gap_start: len,
gap_end: len,
}
}
}
impl Default for GapBuffer {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for GapBuffer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_string())
}
}
impl fmt::Debug for GapBuffer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GapBuffer")
.field("len", &self.len())
.field("gap_start", &self.gap_start)
.field("gap_end", &self.gap_end)
.field("gap_size", &self.gap_size())
.field("text", &self.to_string())
.finish()
}
}
#[inline]
fn utf8_char_len(first_byte: u8) -> usize {
match first_byte {
0x00..=0x7F => 1,
0xC0..=0xDF => 2,
0xE0..=0xEF => 3,
0xF0..=0xF7 => 4,
_ => panic!("utf8_char_len: invalid UTF-8 leading byte 0x{first_byte:02X}"),
}
}
#[inline]
fn is_utf8_start_byte(b: u8) -> bool {
(b & 0xC0) != 0x80
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_buffer_is_empty() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::new();
assert_eq!(buf.len(), 0);
assert!(buf.is_empty());
assert_eq!(buf.char_count(), 0);
assert_eq!(buf.to_string(), "");
}
#[test]
fn from_str_ascii() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("hello");
assert_eq!(buf.len(), 5);
assert_eq!(buf.char_count(), 5);
assert_eq!(buf.to_string(), "hello");
}
#[test]
fn from_str_empty() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("");
assert_eq!(buf.len(), 0);
assert!(buf.is_empty());
assert_eq!(buf.to_string(), "");
}
#[test]
fn insert_at_beginning() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("world");
buf.insert_str(0, "hello ");
assert_eq!(buf.to_string(), "hello world");
}
#[test]
fn insert_at_end() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello");
buf.insert_str(5, " world");
assert_eq!(buf.to_string(), "hello world");
}
#[test]
fn insert_in_middle() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("helo");
buf.insert_str(2, "l");
assert_eq!(buf.to_string(), "hello");
}
#[test]
fn insert_into_empty_buffer() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::new();
buf.insert_str(0, "abc");
assert_eq!(buf.to_string(), "abc");
assert_eq!(buf.len(), 3);
}
#[test]
fn insert_empty_string_is_noop() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello");
buf.insert_str(3, "");
assert_eq!(buf.to_string(), "hello");
}
#[test]
fn multiple_sequential_inserts() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::new();
buf.insert_str(0, "a");
buf.insert_str(1, "b");
buf.insert_str(2, "c");
buf.insert_str(3, "d");
assert_eq!(buf.to_string(), "abcd");
}
#[test]
fn insert_larger_than_gap() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::new();
let long = "x".repeat(256);
buf.insert_str(0, &long);
assert_eq!(buf.to_string(), long);
assert_eq!(buf.len(), 256);
}
#[test]
fn delete_from_beginning() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello world");
buf.delete_range(0, 6);
assert_eq!(buf.to_string(), "world");
}
#[test]
fn delete_from_end() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello world");
buf.delete_range(5, 11);
assert_eq!(buf.to_string(), "hello");
}
#[test]
fn delete_from_middle() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello world");
buf.delete_range(5, 6); assert_eq!(buf.to_string(), "helloworld");
}
#[test]
fn delete_everything() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello");
buf.delete_range(0, 5);
assert_eq!(buf.to_string(), "");
assert!(buf.is_empty());
}
#[test]
fn delete_empty_range_is_noop() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello");
buf.delete_range(2, 2);
assert_eq!(buf.to_string(), "hello");
}
#[test]
fn delete_then_insert() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello world");
buf.delete_range(5, 11);
buf.insert_str(5, " rust");
assert_eq!(buf.to_string(), "hello rust");
}
#[test]
fn byte_at_ascii() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("abcde");
assert_eq!(buf.byte_at(0), b'a');
assert_eq!(buf.byte_at(4), b'e');
}
#[test]
fn byte_at_after_gap_move() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("abcde");
buf.move_gap_to(2);
assert_eq!(buf.byte_at(0), b'a');
assert_eq!(buf.byte_at(2), b'c');
assert_eq!(buf.byte_at(4), b'e');
}
#[test]
fn char_at_ascii() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("hello");
assert_eq!(buf.char_at(0), Some('h'));
assert_eq!(buf.char_at(4), Some('o'));
assert_eq!(buf.char_at(5), None);
}
#[test]
#[should_panic]
fn byte_at_out_of_range_panics() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("hi");
buf.byte_at(2);
}
#[test]
fn text_range_full() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("hello world");
assert_eq!(buf.text_range(0, 11), "hello world");
}
#[test]
fn text_range_prefix() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("hello world");
assert_eq!(buf.text_range(0, 5), "hello");
}
#[test]
fn text_range_suffix() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("hello world");
assert_eq!(buf.text_range(6, 11), "world");
}
#[test]
fn text_range_spanning_gap() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello world");
buf.move_gap_to(5);
assert_eq!(buf.text_range(3, 8), "lo wo");
}
#[test]
fn text_range_empty() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("hello");
assert_eq!(buf.text_range(2, 2), "");
}
#[test]
fn move_gap_to_start() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello");
buf.move_gap_to(0);
assert_eq!(buf.to_string(), "hello");
}
#[test]
fn move_gap_to_end() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello");
buf.move_gap_to(5);
assert_eq!(buf.to_string(), "hello");
}
#[test]
fn move_gap_around() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("abcdef");
buf.move_gap_to(3);
assert_eq!(buf.to_string(), "abcdef");
buf.move_gap_to(0);
assert_eq!(buf.to_string(), "abcdef");
buf.move_gap_to(6);
assert_eq!(buf.to_string(), "abcdef");
buf.move_gap_to(2);
assert_eq!(buf.to_string(), "abcdef");
}
#[test]
fn ensure_gap_grows() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello");
let old_gap = buf.gap_size();
buf.ensure_gap(old_gap + 100);
assert!(buf.gap_size() >= old_gap + 100);
assert_eq!(buf.to_string(), "hello");
}
#[test]
fn ensure_gap_noop_when_large_enough() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello");
let old_gap = buf.gap_size();
buf.ensure_gap(1);
assert_eq!(buf.gap_size(), old_gap);
}
#[test]
fn multibyte_cjk() {
crate::test_utils::init_test_tracing();
let text = "\u{4F60}\u{597D}\u{4E16}\u{754C}"; let buf = GapBuffer::from_str(text);
assert_eq!(buf.len(), 12); assert_eq!(buf.char_count(), 4);
assert_eq!(buf.to_string(), text);
assert_eq!(buf.char_at(0), Some('\u{4F60}')); assert_eq!(buf.char_at(3), Some('\u{597D}')); assert_eq!(buf.char_at(6), Some('\u{4E16}')); assert_eq!(buf.char_at(9), Some('\u{754C}')); }
#[test]
fn multibyte_emoji() {
crate::test_utils::init_test_tracing();
let text = "\u{1F600}\u{1F60D}"; let buf = GapBuffer::from_str(text);
assert_eq!(buf.len(), 8);
assert_eq!(buf.char_count(), 2);
assert_eq!(buf.char_at(0), Some('\u{1F600}'));
assert_eq!(buf.char_at(4), Some('\u{1F60D}'));
}
#[test]
fn insert_multibyte_in_middle() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("ab");
buf.insert_str(1, "\u{1F600}"); assert_eq!(buf.to_string(), "a\u{1F600}b");
assert_eq!(buf.len(), 6); assert_eq!(buf.char_count(), 3);
}
#[test]
fn delete_multibyte_char() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("a\u{4F60}b"); buf.delete_range(1, 4);
assert_eq!(buf.to_string(), "ab");
}
#[test]
fn text_range_multibyte_spanning_gap() {
crate::test_utils::init_test_tracing();
let text = "\u{4F60}\u{597D}\u{4E16}\u{754C}"; let mut buf = GapBuffer::from_str(text);
buf.move_gap_to(6); assert_eq!(buf.text_range(0, 6), "\u{4F60}\u{597D}");
assert_eq!(buf.text_range(6, 12), "\u{4E16}\u{754C}");
assert_eq!(buf.text_range(3, 9), "\u{597D}\u{4E16}");
}
#[test]
fn mixed_ascii_and_multibyte() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello\u{4E16}\u{754C}!");
assert_eq!(buf.len(), 12);
assert_eq!(buf.char_count(), 8);
buf.insert_str(5, " ");
assert_eq!(buf.to_string(), "hello \u{4E16}\u{754C}!");
assert_eq!(buf.len(), 13);
buf.delete_range(6, 12); assert_eq!(buf.to_string(), "hello !");
}
#[test]
fn byte_char_roundtrip_ascii() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("hello");
for i in 0..=5 {
assert_eq!(buf.byte_to_char(i), i);
assert_eq!(buf.char_to_byte(i), i);
}
}
#[test]
fn byte_to_char_cjk() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("\u{4F60}\u{597D}\u{4E16}"); assert_eq!(buf.byte_to_char(0), 0);
assert_eq!(buf.byte_to_char(3), 1);
assert_eq!(buf.byte_to_char(6), 2);
assert_eq!(buf.byte_to_char(9), 3);
}
#[test]
fn char_to_byte_cjk() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("\u{4F60}\u{597D}\u{4E16}"); assert_eq!(buf.char_to_byte(0), 0);
assert_eq!(buf.char_to_byte(1), 3);
assert_eq!(buf.char_to_byte(2), 6);
assert_eq!(buf.char_to_byte(3), 9);
}
#[test]
fn byte_char_roundtrip_mixed() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("a\u{4F60}b");
assert_eq!(buf.byte_to_char(0), 0); assert_eq!(buf.byte_to_char(1), 1); assert_eq!(buf.byte_to_char(4), 2); assert_eq!(buf.byte_to_char(5), 3);
assert_eq!(buf.char_to_byte(0), 0);
assert_eq!(buf.char_to_byte(1), 1);
assert_eq!(buf.char_to_byte(2), 4);
assert_eq!(buf.char_to_byte(3), 5);
}
#[test]
fn byte_char_conversion_with_gap_in_middle() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("a\u{4F60}b\u{597D}c");
buf.move_gap_to(4); assert_eq!(buf.byte_to_char(0), 0);
assert_eq!(buf.byte_to_char(1), 1);
assert_eq!(buf.byte_to_char(4), 2);
assert_eq!(buf.byte_to_char(5), 3);
assert_eq!(buf.byte_to_char(8), 4);
assert_eq!(buf.char_to_byte(0), 0);
assert_eq!(buf.char_to_byte(1), 1);
assert_eq!(buf.char_to_byte(2), 4);
assert_eq!(buf.char_to_byte(3), 5);
assert_eq!(buf.char_to_byte(4), 8);
}
#[test]
fn byte_char_conversion_empty() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::new();
assert_eq!(buf.byte_to_char(0), 0);
assert_eq!(buf.char_to_byte(0), 0);
}
#[test]
fn byte_to_char_emoji() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("x\u{1F600}y"); assert_eq!(buf.byte_to_char(0), 0);
assert_eq!(buf.byte_to_char(1), 1);
assert_eq!(buf.byte_to_char(5), 2);
assert_eq!(buf.byte_to_char(6), 3);
}
#[test]
fn repeated_insert_delete_cycle() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::new();
for i in 0..100 {
let s = format!("{i}");
buf.insert_str(buf.len(), &s);
}
let full = buf.to_string();
assert!(!full.is_empty());
while !buf.is_empty() {
buf.delete_range(0, 1);
}
assert!(buf.is_empty());
assert_eq!(buf.to_string(), "");
}
#[test]
fn gap_moves_correctly_after_multiple_operations() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("the quick brown fox");
buf.delete_range(4, 10); assert_eq!(buf.to_string(), "the brown fox");
buf.insert_str(4, "slow ");
assert_eq!(buf.to_string(), "the slow brown fox");
buf.delete_range(9, 15); assert_eq!(buf.to_string(), "the slow fox");
buf.insert_str(9, "red ");
assert_eq!(buf.to_string(), "the slow red fox");
}
#[test]
fn insert_at_every_position() {
crate::test_utils::init_test_tracing();
for pos in 0..=5 {
let mut buf = GapBuffer::from_str("hello");
buf.insert_str(pos, "X");
assert_eq!(buf.len(), 6);
assert_eq!(buf.byte_at(pos), b'X');
}
}
#[test]
fn display_trait() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("display test");
let s = format!("{buf}");
assert_eq!(s, "display test");
}
#[test]
fn debug_trait_contains_text() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("dbg");
let dbg = format!("{buf:?}");
assert!(dbg.contains("dbg"));
assert!(dbg.contains("GapBuffer"));
}
#[test]
fn default_is_empty() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::default();
assert!(buf.is_empty());
}
#[test]
fn clone_is_independent() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("original");
let clone = buf.clone();
buf.insert_str(0, "X");
assert_eq!(buf.to_string(), "Xoriginal");
assert_eq!(clone.to_string(), "original");
}
#[test]
#[should_panic]
fn insert_past_end_panics() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hi");
buf.insert_str(3, "x");
}
#[test]
#[should_panic]
fn delete_past_end_panics() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hi");
buf.delete_range(0, 3);
}
#[test]
#[should_panic]
fn delete_inverted_range_panics() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hello");
buf.delete_range(3, 1);
}
#[test]
#[should_panic]
fn text_range_past_end_panics() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("hi");
buf.text_range(0, 3);
}
#[test]
#[should_panic]
fn move_gap_past_end_panics() {
crate::test_utils::init_test_tracing();
let mut buf = GapBuffer::from_str("hi");
buf.move_gap_to(3);
}
#[test]
#[should_panic]
fn byte_to_char_past_end_panics() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("hi");
buf.byte_to_char(3);
}
#[test]
fn char_to_byte_past_end_clamps() {
crate::test_utils::init_test_tracing();
let buf = GapBuffer::from_str("hi");
assert_eq!(buf.char_to_byte(3), buf.len());
assert_eq!(buf.char_to_byte(100), buf.len());
}
#[test]
fn copy_bytes_to_basic() {
crate::test_utils::init_test_tracing();
let gb = GapBuffer::from_str("Hello, world!");
let mut out = Vec::new();
gb.copy_bytes_to(0, 5, &mut out);
assert_eq!(&out, b"Hello");
gb.copy_bytes_to(7, 13, &mut out);
assert_eq!(&out, b"world!");
}
#[test]
fn copy_bytes_to_spanning_gap() {
crate::test_utils::init_test_tracing();
let mut gb = GapBuffer::from_str("abcdef");
gb.move_gap_to(3); let mut out = Vec::new();
gb.copy_bytes_to(1, 5, &mut out); assert_eq!(&out, b"bcde");
}
#[test]
fn copy_bytes_to_empty_range() {
crate::test_utils::init_test_tracing();
let gb = GapBuffer::from_str("test");
let mut out = vec![1, 2, 3]; gb.copy_bytes_to(2, 2, &mut out);
assert!(out.is_empty());
}
}