use std::borrow::Cow;
use std::ops::Range;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::coords::{snap_char_boundary, Bias, Point};
use crate::rope::Rope;
pub(crate) const SCAN_WINDOW: u32 = 64 * 1024;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum LoadError {
#[error("document of {len} bytes exceeds the u32 offset space (4 GiB)")]
TooLarge {
len: usize,
},
}
fn check_load_len(len: usize) -> Result<(), LoadError> {
if len >= u32::MAX as usize {
return Err(LoadError::TooLarge { len });
}
Ok(())
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct Revision(pub u64);
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct DocId(u64);
static NEXT_DOC_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum EolFlavor {
Lf,
CrLf,
}
#[derive(Clone, Debug)]
pub struct Snapshot {
text: Rope,
doc_id: DocId,
revision: Revision,
}
impl Snapshot {
#[must_use]
pub fn len(&self) -> u32 {
self.text.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
#[must_use]
pub fn text(&self) -> Cow<'_, str> {
self.text.slice(0..self.text.len())
}
#[must_use]
pub fn slice(&self, range: Range<u32>) -> Cow<'_, str> {
self.text.slice(range)
}
#[must_use]
pub fn line(&self, row: u32) -> Cow<'_, str> {
self.text.line(row)
}
#[must_use]
pub fn line_count(&self) -> u32 {
self.text.line_count()
}
#[must_use]
pub fn doc_id(&self) -> DocId {
self.doc_id
}
#[must_use]
pub fn revision(&self) -> Revision {
self.revision
}
}
#[derive(Clone, Debug)]
pub struct Buffer {
text: Rope,
revision: Revision,
doc_id: DocId,
eol_flavor: EolFlavor,
}
impl Buffer {
pub fn new(input: &str) -> Result<Self, LoadError> {
check_load_len(input.len())?;
let eol_flavor = if input.contains("\r\n") {
EolFlavor::CrLf
} else {
EolFlavor::Lf
};
let text = if input.bytes().any(|b| b == b'\r') {
Rope::from_str(&input.replace("\r\n", "\n").replace('\r', "\n"))
} else {
Rope::from_str(input)
};
Ok(Self {
text,
revision: Revision(0),
doc_id: DocId(NEXT_DOC_ID.fetch_add(1, Ordering::Relaxed)),
eol_flavor,
})
}
#[must_use]
pub fn len(&self) -> u32 {
self.text.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
#[must_use]
pub fn text(&self) -> Cow<'_, str> {
self.text.slice(0..self.text.len())
}
#[must_use]
pub fn slice(&self, range: Range<u32>) -> Cow<'_, str> {
self.text.slice(range)
}
#[must_use]
pub fn line(&self, row: u32) -> Cow<'_, str> {
self.text.line(row)
}
#[must_use]
pub fn char_at(&self, offset: u32) -> Option<char> {
let off = offset.min(self.len());
let (chunk, chunk_start) = self.text.chunk_at(off);
chunk[(off - chunk_start) as usize..].chars().next()
}
#[must_use]
pub fn char_before(&self, offset: u32) -> Option<char> {
let off = offset.min(self.len());
if off == 0 {
return None;
}
let (chunk, chunk_start) = self.text.chunk_at(off - 1);
chunk[..(off - chunk_start) as usize].chars().next_back()
}
#[must_use]
pub fn line_count(&self) -> u32 {
self.text.line_count()
}
#[must_use]
pub fn line_len(&self, row: u32) -> u32 {
self.text.line_len(row)
}
#[must_use]
pub fn revision(&self) -> Revision {
self.revision
}
#[must_use]
pub fn doc_id(&self) -> DocId {
self.doc_id
}
#[must_use]
pub fn eol_flavor(&self) -> EolFlavor {
self.eol_flavor
}
#[must_use]
pub fn offset_to_point(&self, offset: u32) -> Point {
crate::perf::charge(1); self.text.byte_to_point(offset)
}
#[must_use]
pub fn point_to_offset(&self, point: Point) -> u32 {
crate::perf::charge(1); self.text.point_to_offset(point)
}
#[must_use]
pub fn clip_offset(&self, offset: u32, bias: Bias) -> u32 {
let off = offset.min(self.len());
let (chunk, chunk_start) = self.text.chunk_at(off);
chunk_start + snap_char_boundary(chunk, off - chunk_start, bias)
}
#[must_use]
pub(crate) fn scan_resume(&self, pos: u32, win_end: u32, k: u32, last_match_end: Option<u32>) -> u32 {
match last_match_end {
Some(end) => end,
None => {
let safe = self.clip_offset((u64::from(win_end) - u64::from(k - 1)) as u32, Bias::Left);
safe.max(self.clip_offset(pos + 1, Bias::Right))
}
}
}
#[must_use]
pub fn clip_point(&self, point: Point, bias: Bias) -> Point {
self.offset_to_point(self.clip_offset(self.point_to_offset(point), bias))
}
#[must_use]
pub fn snapshot(&self) -> Snapshot {
Snapshot { text: self.text.clone(), doc_id: self.doc_id, revision: self.revision }
}
#[must_use]
pub fn serialize(&self, flavor: EolFlavor) -> String {
match flavor {
EolFlavor::Lf => {
let mut out = String::with_capacity(self.text.len() as usize);
self.text.for_each_chunk(|chunk| out.push_str(chunk));
out
}
EolFlavor::CrLf => {
let extra = self.text.line_count() as usize - 1;
let mut out = String::with_capacity(self.text.len() as usize + extra);
for chunk in self.text.chunks() {
let mut rest = chunk;
while let Some(i) = rest.find('\n') {
out.push_str(&rest[..i]);
out.push_str("\r\n");
rest = &rest[i + 1..];
}
out.push_str(rest);
}
out
}
}
}
pub(crate) fn splice(&mut self, range: Range<u32>, new_text: &str) {
debug_assert!(!new_text.as_bytes().contains(&b'\r'), "splice text must be LF-only");
let (start, end) = (range.start, range.end);
debug_assert!(start <= end && end <= self.len(), "splice range out of bounds");
debug_assert!(self.text.is_char_boundary(start), "splice start is not a char boundary");
debug_assert!(self.text.is_char_boundary(end), "splice end is not a char boundary");
self.text.replace(start..end, new_text);
}
pub(crate) fn edit_many(&mut self, edits: &[(Range<u32>, &str)]) {
debug_assert!(
edits.windows(2).all(|w| w[0].0.end <= w[1].0.start),
"edit_many wants sorted, disjoint ranges"
);
debug_assert!(
edits.iter().all(|(r, t)| {
r.end <= self.len()
&& self.text.is_char_boundary(r.start)
&& self.text.is_char_boundary(r.end)
&& !t.as_bytes().contains(&b'\r')
}),
"edit_many ranges must be in-bounds char boundaries and text LF-only"
);
match edits {
[] => {}
[(range, text)] => self.splice(range.clone(), text),
_ => self.text.edit_many(edits),
}
}
pub(crate) fn bump_revision(&mut self) {
self.revision.0 += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn buf(s: &str) -> Buffer {
Buffer::new(s).unwrap()
}
#[test]
fn empty_document_is_one_empty_line() {
let b = buf("");
assert_eq!(b.line_count(), 1);
assert_eq!(b.line(0), "");
assert_eq!(b.len(), 0);
assert!(b.is_empty());
}
#[test]
fn trailing_newline_is_an_empty_final_line() {
let b = buf("foo\nbar\n");
assert_eq!(b.line_count(), 3);
assert_eq!(b.line(0), "foo");
assert_eq!(b.line(1), "bar");
assert_eq!(b.line(2), "");
let b2 = buf("foo\nbar");
assert_eq!(b2.line_count(), 2);
assert_eq!(b2.line(1), "bar");
}
#[test]
fn crlf_normalized_on_load_flavor_remembered() {
let b = buf("a\r\nb\r\n");
assert_eq!(b.text(), "a\nb\n"); assert_eq!(b.eol_flavor(), EolFlavor::CrLf);
assert!(!b.text().contains('\r'));
assert_eq!(buf("a\nb").eol_flavor(), EolFlavor::Lf);
}
#[test]
fn serialize_round_trips_each_flavor() {
let original = "a\r\nb\r\n";
let b = buf(original);
assert_eq!(b.serialize(EolFlavor::CrLf), original);
assert_eq!(b.serialize(EolFlavor::Lf), "a\nb\n");
let b2 = buf("x\ny");
assert_eq!(b2.serialize(EolFlavor::Lf), "x\ny");
}
#[test]
fn only_the_u32_offset_space_is_refused() {
assert!(Buffer::new(&"a".repeat(2 * 1_048_576)).is_ok());
assert!(check_load_len(u32::MAX as usize - 1).is_ok());
assert!(matches!(
check_load_len(u32::MAX as usize),
Err(LoadError::TooLarge { len }) if len == u32::MAX as usize
));
assert!(check_load_len(u32::MAX as usize + 1).is_err());
}
#[test]
fn bump_revision_increments() {
let mut b = buf("x");
assert_eq!(b.revision(), Revision(0));
b.bump_revision();
assert_eq!(b.revision(), Revision(1));
}
#[test]
fn offset_point_round_trip() {
let b = buf("foo\nbar\nbaz");
for off in 0..=b.len() {
let p = b.offset_to_point(off);
assert_eq!(b.point_to_offset(p), off, "offset {off}");
}
assert_eq!(b.offset_to_point(0), Point::new(0, 0));
assert_eq!(b.offset_to_point(4), Point::new(1, 0)); assert_eq!(b.offset_to_point(6), Point::new(1, 2)); }
#[test]
fn point_to_offset_clamps_row_and_col() {
let b = buf("ab\ncd");
assert_eq!(b.point_to_offset(Point::new(0, 99)), 2); assert_eq!(b.point_to_offset(Point::new(9, 0)), 3); }
#[test]
fn splice_insert_updates_index_and_text() {
let mut b = buf("ac");
b.splice(1..1, "b"); assert_eq!(b.text(), "abc");
assert_eq!(b.line_count(), 1);
let mut b = buf("line1\nline2");
b.splice(5..5, "X\nY"); assert_eq!(b.text(), "line1X\nY\nline2");
assert_eq!(b.line_count(), 3);
assert_eq!(b.line(0), "line1X");
assert_eq!(b.line(1), "Y");
assert_eq!(b.line(2), "line2");
}
#[test]
fn splice_delete_across_lines_merges() {
let mut b = buf("foo\nbar\nbaz");
b.splice(2..9, ""); assert_eq!(b.text(), "foaz");
assert_eq!(b.line_count(), 1);
}
#[test]
fn splice_replace_shifts_following_line_starts() {
let mut b = buf("a\nb\nc");
b.splice(0..1, "AAAA");
assert_eq!(b.text(), "AAAA\nb\nc");
assert_eq!(b.line(0), "AAAA");
assert_eq!(b.line(1), "b");
assert_eq!(b.line(2), "c");
}
#[test]
fn only_lf_is_a_line_break() {
let b = buf("a\u{000B}b\u{000C}c\u{0085}d\u{2028}e\u{2029}f\ng");
assert_eq!(b.line_count(), 2, "only \\n may break lines");
assert_eq!(b.line(1), "g");
assert_eq!(b.offset_to_point(b.len()).row, 1);
}
#[test]
fn chunk_crossing_reads_match_a_string_model() {
let mut model = String::new();
for i in 0..2000 {
match i % 4 {
0 => model.push_str(&format!("line {i}: the quick brown fox\n")),
1 => model.push_str(&format!("Zeile {i}: äöü ßẞ €42 →←\n")),
2 => model.push_str(&format!("行 {i}: 日本語のテキスト 🦀🚀\n")),
_ => model.push_str(&format!("l{i}\n")),
}
}
model.push_str("no trailing newline");
let b = buf(&model);
assert_eq!(b.len() as usize, model.len());
assert_eq!(b.text(), model.as_str());
let mut starts = vec![0usize];
starts.extend(memchr::memchr_iter(b'\n', model.as_bytes()).map(|i| i + 1));
assert_eq!(b.line_count() as usize, starts.len());
for (row, &start) in starts.iter().enumerate() {
let end = starts.get(row + 1).map_or(model.len(), |&next| next - 1);
assert_eq!(b.line(row as u32), &model[start..end], "line {row}");
assert_eq!(b.line_len(row as u32) as usize, end - start, "line_len {row}");
}
let mut off = 0usize;
while off <= model.len() {
let o = off as u32;
assert_eq!(b.char_at(o), model[off..].chars().next(), "char_at {off}");
assert_eq!(b.char_before(o), model[..off].chars().next_back(), "char_before {off}");
let p = b.offset_to_point(o);
assert_eq!(b.point_to_offset(p), o, "point round-trip {off}");
assert_eq!(starts[p.row as usize] + p.col as usize, off, "offset_to_point {off}");
assert_eq!(b.clip_offset(o, Bias::Left), o, "boundary clips are identity");
let slice_end = (off + 97).min(model.len());
let slice_end = (slice_end..=model.len())
.find(|&e| model.is_char_boundary(e))
.unwrap_or(model.len());
assert_eq!(b.slice(o..slice_end as u32), &model[off..slice_end], "slice at {off}");
off = (off + 61..=model.len())
.find(|&e| model.is_char_boundary(e))
.unwrap_or(model.len() + 1);
}
for (i, _) in model.char_indices().take(500) {
for probe in i + 1..(i + 4).min(model.len()) {
if !model.is_char_boundary(probe) {
let left = (0..=probe).rev().find(|&x| model.is_char_boundary(x)).unwrap();
let right = (probe..=model.len()).find(|&x| model.is_char_boundary(x)).unwrap();
assert_eq!(b.clip_offset(probe as u32, Bias::Left) as usize, left);
assert_eq!(b.clip_offset(probe as u32, Bias::Right) as usize, right);
}
}
}
let seams: Vec<usize> = {
let mut acc = 0usize;
b.text
.chunks()
.map(|c| {
acc += c.len();
acc
})
.collect()
};
assert!(seams.len() > 1, "corpus must span multiple chunks");
for &seam in &seams[..seams.len() - 1] {
for probe in [seam - 1, seam, seam + 1] {
let o = probe as u32;
if model.is_char_boundary(probe) {
assert_eq!(b.char_at(o), model[probe..].chars().next(), "char_at seam {probe}");
assert_eq!(
b.char_before(o),
model[..probe].chars().next_back(),
"char_before seam {probe}"
);
assert_eq!(b.clip_offset(o, Bias::Left), o, "seam clip identity {probe}");
let end = ((probe + 5).min(model.len())..=model.len())
.find(|&e| model.is_char_boundary(e))
.unwrap();
assert_eq!(b.slice(o..end as u32), &model[probe..end], "slice seam {probe}");
} else {
let left = (0..=probe).rev().find(|&x| model.is_char_boundary(x)).unwrap();
let right = (probe..=model.len()).find(|&x| model.is_char_boundary(x)).unwrap();
assert_eq!(b.clip_offset(o, Bias::Left) as usize, left, "seam {probe}");
assert_eq!(b.clip_offset(o, Bias::Right) as usize, right, "seam {probe}");
}
}
}
}
#[test]
fn splice_random_walk_matches_string_model() {
let mut model = String::new();
for i in 0..400 {
model.push_str(&format!("seed line {i}: ää🦀 with some text\n"));
}
let mut b = buf(&model);
let mut state = 0x2545F49_u64; let mut rand = move |bound: usize| {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
((state >> 33) as usize) % bound.max(1)
};
let inserts =
["x", "hello", "\n", "ab\ncd", "äöü", "🦀", "fn main() {}\n", "", "日本語", "\n\n\n"];
let mut multi_chunk_steps = 0;
for step in 0..600 {
let mut s = rand(model.len() + 1);
while !model.is_char_boundary(s) {
s -= 1;
}
let mut e = (s + rand(24)).min(model.len());
while !model.is_char_boundary(e) {
e -= 1;
}
let e = e.max(s);
let ins = inserts[rand(inserts.len())];
model.replace_range(s..e, ins);
b.splice(s as u32..e as u32, ins);
assert_eq!(b.text(), model.as_str(), "text diverged at step {step}");
assert_eq!(
b.line_count() as usize,
memchr::memchr_iter(b'\n', model.as_bytes()).count() + 1,
"line count diverged at step {step}"
);
if b.text.chunks().nth(1).is_some() {
multi_chunk_steps += 1;
}
}
assert!(
multi_chunk_steps >= 550,
"walk must stay multi-chunk (got {multi_chunk_steps}/600 steps)"
);
}
#[test]
#[allow(clippy::reversed_empty_ranges)] fn slice_clamps_inverted_and_past_end_ranges() {
let b = buf("hello world");
assert_eq!(b.slice(5..2), "");
assert_eq!(b.slice(100..3), "");
assert_eq!(b.slice(3..100), "lo world");
assert_eq!(b.slice(100..200), "");
assert_eq!(b.line(99), "");
let snap = b.snapshot();
assert_eq!(snap.slice(5..2), "");
assert_eq!(snap.slice(3..100), "lo world");
assert_eq!(snap.line(99), "");
}
#[test]
fn snapshot_is_isolated_from_later_edits() {
let mut b = buf("alpha\nbeta\n");
b.bump_revision();
let snap = b.snapshot();
b.splice(0..5, "OMEGA");
assert_eq!(snap.text(), "alpha\nbeta\n");
assert_eq!(snap.line(0), "alpha");
assert_eq!(snap.slice(6..10), "beta");
assert_eq!(snap.line_count(), 3);
assert_eq!(snap.len(), 11);
assert!(!snap.is_empty());
assert_eq!(snap.revision(), Revision(1));
assert_eq!(snap.doc_id(), b.doc_id());
assert_eq!(b.text(), "OMEGA\nbeta\n", "the buffer moved on");
}
#[test]
fn small_document_reads_are_borrowed() {
let b = buf("let x = 1;\nlet y = 2;\n");
assert!(matches!(b.text(), Cow::Borrowed(_)));
assert!(matches!(b.slice(4..9), Cow::Borrowed(_)));
assert!(matches!(b.line(1), Cow::Borrowed(_)));
}
}