From a65c1776d8226fa4483272d2e1b8e56d543041fa Mon Sep 17 00:00:00 2001 From: KCaverly Date: Fri, 3 Nov 2023 12:15:23 -0400 Subject: [PATCH] port rope2 to zed2 --- Cargo.lock | 19 +- crates/fs2/Cargo.toml | 2 +- crates/rope2/Cargo.toml | 21 + crates/rope2/src/offset_utf16.rs | 50 ++ crates/rope2/src/point.rs | 128 +++ crates/rope2/src/point_utf16.rs | 119 +++ crates/rope2/src/rope2.rs | 1433 ++++++++++++++++++++++++++++++ crates/rope2/src/unclipped.rs | 57 ++ crates/text2/Cargo.toml | 2 +- crates/zed2/Cargo.toml | 1 + 10 files changed, 1828 insertions(+), 4 deletions(-) create mode 100644 crates/rope2/Cargo.toml create mode 100644 crates/rope2/src/offset_utf16.rs create mode 100644 crates/rope2/src/point.rs create mode 100644 crates/rope2/src/point_utf16.rs create mode 100644 crates/rope2/src/rope2.rs create mode 100644 crates/rope2/src/unclipped.rs diff --git a/Cargo.lock b/Cargo.lock index d5d0493936..27126326a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3125,7 +3125,7 @@ dependencies = [ "log", "parking_lot 0.11.2", "regex", - "rope", + "rope2", "serde", "serde_derive", "serde_json", @@ -7106,6 +7106,20 @@ dependencies = [ "util", ] +[[package]] +name = "rope2" +version = "0.1.0" +dependencies = [ + "arrayvec 0.7.4", + "bromberg_sl2", + "gpui2", + "log", + "rand 0.8.5", + "smallvec", + "sum_tree", + "util", +] + [[package]] name = "roxmltree" version = "0.14.1" @@ -8911,7 +8925,7 @@ dependencies = [ "postage", "rand 0.8.5", "regex", - "rope", + "rope2", "smallvec", "sum_tree", "util", @@ -11189,6 +11203,7 @@ dependencies = [ "project2", "rand 0.8.5", "regex", + "rope2", "rpc2", "rsa 0.4.0", "rust-embed", diff --git a/crates/fs2/Cargo.toml b/crates/fs2/Cargo.toml index ca525afe5f..46ff7a274e 100644 --- a/crates/fs2/Cargo.toml +++ b/crates/fs2/Cargo.toml @@ -9,7 +9,7 @@ path = "src/fs2.rs" [dependencies] collections = { path = "../collections" } -rope = { path = "../rope" } +rope = { package = "rope2", path = "../rope2" } text = { package = "text2", path = "../text2" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } diff --git a/crates/rope2/Cargo.toml b/crates/rope2/Cargo.toml new file mode 100644 index 0000000000..2ef247f418 --- /dev/null +++ b/crates/rope2/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "rope2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/rope2.rs" + +[dependencies] +bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "950bc5482c216c395049ae33ae4501e08975f17f" } +smallvec.workspace = true +sum_tree = { path = "../sum_tree" } +arrayvec = "0.7.1" +log.workspace = true +util = { path = "../util" } + +[dev-dependencies] +rand.workspace = true +util = { path = "../util", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } diff --git a/crates/rope2/src/offset_utf16.rs b/crates/rope2/src/offset_utf16.rs new file mode 100644 index 0000000000..9a52b3c3f9 --- /dev/null +++ b/crates/rope2/src/offset_utf16.rs @@ -0,0 +1,50 @@ +use std::ops::{Add, AddAssign, Sub}; + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] +pub struct OffsetUtf16(pub usize); + +impl<'a> Add<&'a Self> for OffsetUtf16 { + type Output = Self; + + fn add(self, other: &'a Self) -> Self::Output { + Self(self.0 + other.0) + } +} + +impl Add for OffsetUtf16 { + type Output = Self; + + fn add(self, other: Self) -> Self::Output { + Self(self.0 + other.0) + } +} + +impl<'a> Sub<&'a Self> for OffsetUtf16 { + type Output = Self; + + fn sub(self, other: &'a Self) -> Self::Output { + debug_assert!(*other <= self); + Self(self.0 - other.0) + } +} + +impl Sub for OffsetUtf16 { + type Output = OffsetUtf16; + + fn sub(self, other: Self) -> Self::Output { + debug_assert!(other <= self); + Self(self.0 - other.0) + } +} + +impl<'a> AddAssign<&'a Self> for OffsetUtf16 { + fn add_assign(&mut self, other: &'a Self) { + self.0 += other.0; + } +} + +impl AddAssign for OffsetUtf16 { + fn add_assign(&mut self, other: Self) { + self.0 += other.0; + } +} diff --git a/crates/rope2/src/point.rs b/crates/rope2/src/point.rs new file mode 100644 index 0000000000..4be01dfdac --- /dev/null +++ b/crates/rope2/src/point.rs @@ -0,0 +1,128 @@ +use std::{ + cmp::Ordering, + ops::{Add, AddAssign, Sub}, +}; + +#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash)] +pub struct Point { + pub row: u32, + pub column: u32, +} + +impl Point { + pub const MAX: Self = Self { + row: u32::MAX, + column: u32::MAX, + }; + + pub fn new(row: u32, column: u32) -> Self { + Point { row, column } + } + + pub fn zero() -> Self { + Point::new(0, 0) + } + + pub fn parse_str(s: &str) -> Self { + let mut point = Self::zero(); + for (row, line) in s.split('\n').enumerate() { + point.row = row as u32; + point.column = line.len() as u32; + } + point + } + + pub fn is_zero(&self) -> bool { + self.row == 0 && self.column == 0 + } + + pub fn saturating_sub(self, other: Self) -> Self { + if self < other { + Self::zero() + } else { + self - other + } + } +} + +impl<'a> Add<&'a Self> for Point { + type Output = Point; + + fn add(self, other: &'a Self) -> Self::Output { + self + *other + } +} + +impl Add for Point { + type Output = Point; + + fn add(self, other: Self) -> Self::Output { + if other.row == 0 { + Point::new(self.row, self.column + other.column) + } else { + Point::new(self.row + other.row, other.column) + } + } +} + +impl<'a> Sub<&'a Self> for Point { + type Output = Point; + + fn sub(self, other: &'a Self) -> Self::Output { + self - *other + } +} + +impl Sub for Point { + type Output = Point; + + fn sub(self, other: Self) -> Self::Output { + debug_assert!(other <= self); + + if self.row == other.row { + Point::new(0, self.column - other.column) + } else { + Point::new(self.row - other.row, self.column) + } + } +} + +impl<'a> AddAssign<&'a Self> for Point { + fn add_assign(&mut self, other: &'a Self) { + *self += *other; + } +} + +impl AddAssign for Point { + fn add_assign(&mut self, other: Self) { + if other.row == 0 { + self.column += other.column; + } else { + self.row += other.row; + self.column = other.column; + } + } +} + +impl PartialOrd for Point { + fn partial_cmp(&self, other: &Point) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Point { + #[cfg(target_pointer_width = "64")] + fn cmp(&self, other: &Point) -> Ordering { + let a = (self.row as usize) << 32 | self.column as usize; + let b = (other.row as usize) << 32 | other.column as usize; + a.cmp(&b) + } + + #[cfg(target_pointer_width = "32")] + fn cmp(&self, other: &Point) -> Ordering { + match self.row.cmp(&other.row) { + Ordering::Equal => self.column.cmp(&other.column), + comparison @ _ => comparison, + } + } +} diff --git a/crates/rope2/src/point_utf16.rs b/crates/rope2/src/point_utf16.rs new file mode 100644 index 0000000000..7b76ee3776 --- /dev/null +++ b/crates/rope2/src/point_utf16.rs @@ -0,0 +1,119 @@ +use std::{ + cmp::Ordering, + ops::{Add, AddAssign, Sub}, +}; + +#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash)] +pub struct PointUtf16 { + pub row: u32, + pub column: u32, +} + +impl PointUtf16 { + pub const MAX: Self = Self { + row: u32::MAX, + column: u32::MAX, + }; + + pub fn new(row: u32, column: u32) -> Self { + PointUtf16 { row, column } + } + + pub fn zero() -> Self { + PointUtf16::new(0, 0) + } + + pub fn is_zero(&self) -> bool { + self.row == 0 && self.column == 0 + } + + pub fn saturating_sub(self, other: Self) -> Self { + if self < other { + Self::zero() + } else { + self - other + } + } +} + +impl<'a> Add<&'a Self> for PointUtf16 { + type Output = PointUtf16; + + fn add(self, other: &'a Self) -> Self::Output { + self + *other + } +} + +impl Add for PointUtf16 { + type Output = PointUtf16; + + fn add(self, other: Self) -> Self::Output { + if other.row == 0 { + PointUtf16::new(self.row, self.column + other.column) + } else { + PointUtf16::new(self.row + other.row, other.column) + } + } +} + +impl<'a> Sub<&'a Self> for PointUtf16 { + type Output = PointUtf16; + + fn sub(self, other: &'a Self) -> Self::Output { + self - *other + } +} + +impl Sub for PointUtf16 { + type Output = PointUtf16; + + fn sub(self, other: Self) -> Self::Output { + debug_assert!(other <= self); + + if self.row == other.row { + PointUtf16::new(0, self.column - other.column) + } else { + PointUtf16::new(self.row - other.row, self.column) + } + } +} + +impl<'a> AddAssign<&'a Self> for PointUtf16 { + fn add_assign(&mut self, other: &'a Self) { + *self += *other; + } +} + +impl AddAssign for PointUtf16 { + fn add_assign(&mut self, other: Self) { + if other.row == 0 { + self.column += other.column; + } else { + self.row += other.row; + self.column = other.column; + } + } +} + +impl PartialOrd for PointUtf16 { + fn partial_cmp(&self, other: &PointUtf16) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PointUtf16 { + #[cfg(target_pointer_width = "64")] + fn cmp(&self, other: &PointUtf16) -> Ordering { + let a = (self.row as usize) << 32 | self.column as usize; + let b = (other.row as usize) << 32 | other.column as usize; + a.cmp(&b) + } + + #[cfg(target_pointer_width = "32")] + fn cmp(&self, other: &PointUtf16) -> Ordering { + match self.row.cmp(&other.row) { + Ordering::Equal => self.column.cmp(&other.column), + comparison @ _ => comparison, + } + } +} diff --git a/crates/rope2/src/rope2.rs b/crates/rope2/src/rope2.rs new file mode 100644 index 0000000000..9c764c468e --- /dev/null +++ b/crates/rope2/src/rope2.rs @@ -0,0 +1,1433 @@ +mod offset_utf16; +mod point; +mod point_utf16; +mod unclipped; + +use arrayvec::ArrayString; +use bromberg_sl2::HashMatrix; +use smallvec::SmallVec; +use std::{ + cmp, fmt, io, mem, + ops::{AddAssign, Range}, + str, +}; +use sum_tree::{Bias, Dimension, SumTree}; +use util::debug_panic; + +pub use offset_utf16::OffsetUtf16; +pub use point::Point; +pub use point_utf16::PointUtf16; +pub use unclipped::Unclipped; + +#[cfg(test)] +const CHUNK_BASE: usize = 6; + +#[cfg(not(test))] +const CHUNK_BASE: usize = 16; + +/// Type alias to [HashMatrix], an implementation of a homomorphic hash function. Two [Rope] instances +/// containing the same text will produce the same fingerprint. This hash function is special in that +/// it allows us to hash individual chunks and aggregate them up the [Rope]'s tree, with the resulting +/// hash being equivalent to hashing all the text contained in the [Rope] at once. +pub type RopeFingerprint = HashMatrix; + +#[derive(Clone, Default)] +pub struct Rope { + chunks: SumTree, +} + +impl Rope { + pub fn new() -> Self { + Self::default() + } + + pub fn append(&mut self, rope: Rope) { + let mut chunks = rope.chunks.cursor::<()>(); + chunks.next(&()); + if let Some(chunk) = chunks.item() { + if self.chunks.last().map_or(false, |c| c.0.len() < CHUNK_BASE) + || chunk.0.len() < CHUNK_BASE + { + self.push(&chunk.0); + chunks.next(&()); + } + } + + self.chunks.append(chunks.suffix(&()), &()); + self.check_invariants(); + } + + pub fn replace(&mut self, range: Range, text: &str) { + let mut new_rope = Rope::new(); + let mut cursor = self.cursor(0); + new_rope.append(cursor.slice(range.start)); + cursor.seek_forward(range.end); + new_rope.push(text); + new_rope.append(cursor.suffix()); + *self = new_rope; + } + + pub fn slice(&self, range: Range) -> Rope { + let mut cursor = self.cursor(0); + cursor.seek_forward(range.start); + cursor.slice(range.end) + } + + pub fn slice_rows(&self, range: Range) -> Rope { + //This would be more efficient with a forward advance after the first, but it's fine + let start = self.point_to_offset(Point::new(range.start, 0)); + let end = self.point_to_offset(Point::new(range.end, 0)); + self.slice(start..end) + } + + pub fn push(&mut self, text: &str) { + let mut new_chunks = SmallVec::<[_; 16]>::new(); + let mut new_chunk = ArrayString::new(); + for ch in text.chars() { + if new_chunk.len() + ch.len_utf8() > 2 * CHUNK_BASE { + new_chunks.push(Chunk(new_chunk)); + new_chunk = ArrayString::new(); + } + + new_chunk.push(ch); + } + if !new_chunk.is_empty() { + new_chunks.push(Chunk(new_chunk)); + } + + let mut new_chunks = new_chunks.into_iter(); + let mut first_new_chunk = new_chunks.next(); + self.chunks.update_last( + |last_chunk| { + if let Some(first_new_chunk_ref) = first_new_chunk.as_mut() { + if last_chunk.0.len() + first_new_chunk_ref.0.len() <= 2 * CHUNK_BASE { + last_chunk.0.push_str(&first_new_chunk.take().unwrap().0); + } else { + let mut text = ArrayString::<{ 4 * CHUNK_BASE }>::new(); + text.push_str(&last_chunk.0); + text.push_str(&first_new_chunk_ref.0); + let (left, right) = text.split_at(find_split_ix(&text)); + last_chunk.0.clear(); + last_chunk.0.push_str(left); + first_new_chunk_ref.0.clear(); + first_new_chunk_ref.0.push_str(right); + } + } + }, + &(), + ); + + self.chunks + .extend(first_new_chunk.into_iter().chain(new_chunks), &()); + self.check_invariants(); + } + + pub fn push_front(&mut self, text: &str) { + let suffix = mem::replace(self, Rope::from(text)); + self.append(suffix); + } + + fn check_invariants(&self) { + #[cfg(test)] + { + // Ensure all chunks except maybe the last one are not underflowing. + // Allow some wiggle room for multibyte characters at chunk boundaries. + let mut chunks = self.chunks.cursor::<()>().peekable(); + while let Some(chunk) = chunks.next() { + if chunks.peek().is_some() { + assert!(chunk.0.len() + 3 >= CHUNK_BASE); + } + } + } + } + + pub fn summary(&self) -> TextSummary { + self.chunks.summary().text.clone() + } + + pub fn len(&self) -> usize { + self.chunks.extent(&()) + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn max_point(&self) -> Point { + self.chunks.extent(&()) + } + + pub fn max_point_utf16(&self) -> PointUtf16 { + self.chunks.extent(&()) + } + + pub fn cursor(&self, offset: usize) -> Cursor { + Cursor::new(self, offset) + } + + pub fn chars(&self) -> impl Iterator + '_ { + self.chars_at(0) + } + + pub fn chars_at(&self, start: usize) -> impl Iterator + '_ { + self.chunks_in_range(start..self.len()).flat_map(str::chars) + } + + pub fn reversed_chars_at(&self, start: usize) -> impl Iterator + '_ { + self.reversed_chunks_in_range(0..start) + .flat_map(|chunk| chunk.chars().rev()) + } + + pub fn bytes_in_range(&self, range: Range) -> Bytes { + Bytes::new(self, range, false) + } + + pub fn reversed_bytes_in_range(&self, range: Range) -> Bytes { + Bytes::new(self, range, true) + } + + pub fn chunks(&self) -> Chunks { + self.chunks_in_range(0..self.len()) + } + + pub fn chunks_in_range(&self, range: Range) -> Chunks { + Chunks::new(self, range, false) + } + + pub fn reversed_chunks_in_range(&self, range: Range) -> Chunks { + Chunks::new(self, range, true) + } + + pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { + if offset >= self.summary().len { + return self.summary().len_utf16; + } + let mut cursor = self.chunks.cursor::<(usize, OffsetUtf16)>(); + cursor.seek(&offset, Bias::Left, &()); + let overshoot = offset - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(Default::default(), |chunk| { + chunk.offset_to_offset_utf16(overshoot) + }) + } + + pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { + if offset >= self.summary().len_utf16 { + return self.summary().len; + } + let mut cursor = self.chunks.cursor::<(OffsetUtf16, usize)>(); + cursor.seek(&offset, Bias::Left, &()); + let overshoot = offset - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(Default::default(), |chunk| { + chunk.offset_utf16_to_offset(overshoot) + }) + } + + pub fn offset_to_point(&self, offset: usize) -> Point { + if offset >= self.summary().len { + return self.summary().lines; + } + let mut cursor = self.chunks.cursor::<(usize, Point)>(); + cursor.seek(&offset, Bias::Left, &()); + let overshoot = offset - cursor.start().0; + cursor.start().1 + + cursor + .item() + .map_or(Point::zero(), |chunk| chunk.offset_to_point(overshoot)) + } + + pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { + if offset >= self.summary().len { + return self.summary().lines_utf16(); + } + let mut cursor = self.chunks.cursor::<(usize, PointUtf16)>(); + cursor.seek(&offset, Bias::Left, &()); + let overshoot = offset - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(PointUtf16::zero(), |chunk| { + chunk.offset_to_point_utf16(overshoot) + }) + } + + pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { + if point >= self.summary().lines { + return self.summary().lines_utf16(); + } + let mut cursor = self.chunks.cursor::<(Point, PointUtf16)>(); + cursor.seek(&point, Bias::Left, &()); + let overshoot = point - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(PointUtf16::zero(), |chunk| { + chunk.point_to_point_utf16(overshoot) + }) + } + + pub fn point_to_offset(&self, point: Point) -> usize { + if point >= self.summary().lines { + return self.summary().len; + } + let mut cursor = self.chunks.cursor::<(Point, usize)>(); + cursor.seek(&point, Bias::Left, &()); + let overshoot = point - cursor.start().0; + cursor.start().1 + + cursor + .item() + .map_or(0, |chunk| chunk.point_to_offset(overshoot)) + } + + pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { + self.point_utf16_to_offset_impl(point, false) + } + + pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped) -> usize { + self.point_utf16_to_offset_impl(point.0, true) + } + + fn point_utf16_to_offset_impl(&self, point: PointUtf16, clip: bool) -> usize { + if point >= self.summary().lines_utf16() { + return self.summary().len; + } + let mut cursor = self.chunks.cursor::<(PointUtf16, usize)>(); + cursor.seek(&point, Bias::Left, &()); + let overshoot = point - cursor.start().0; + cursor.start().1 + + cursor + .item() + .map_or(0, |chunk| chunk.point_utf16_to_offset(overshoot, clip)) + } + + pub fn unclipped_point_utf16_to_point(&self, point: Unclipped) -> Point { + if point.0 >= self.summary().lines_utf16() { + return self.summary().lines; + } + let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>(); + cursor.seek(&point.0, Bias::Left, &()); + let overshoot = Unclipped(point.0 - cursor.start().0); + cursor.start().1 + + cursor.item().map_or(Point::zero(), |chunk| { + chunk.unclipped_point_utf16_to_point(overshoot) + }) + } + + pub fn clip_offset(&self, mut offset: usize, bias: Bias) -> usize { + let mut cursor = self.chunks.cursor::(); + cursor.seek(&offset, Bias::Left, &()); + if let Some(chunk) = cursor.item() { + let mut ix = offset - cursor.start(); + while !chunk.0.is_char_boundary(ix) { + match bias { + Bias::Left => { + ix -= 1; + offset -= 1; + } + Bias::Right => { + ix += 1; + offset += 1; + } + } + } + offset + } else { + self.summary().len + } + } + + pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + let mut cursor = self.chunks.cursor::(); + cursor.seek(&offset, Bias::Right, &()); + if let Some(chunk) = cursor.item() { + let overshoot = offset - cursor.start(); + *cursor.start() + chunk.clip_offset_utf16(overshoot, bias) + } else { + self.summary().len_utf16 + } + } + + pub fn clip_point(&self, point: Point, bias: Bias) -> Point { + let mut cursor = self.chunks.cursor::(); + cursor.seek(&point, Bias::Right, &()); + if let Some(chunk) = cursor.item() { + let overshoot = point - cursor.start(); + *cursor.start() + chunk.clip_point(overshoot, bias) + } else { + self.summary().lines + } + } + + pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { + let mut cursor = self.chunks.cursor::(); + cursor.seek(&point.0, Bias::Right, &()); + if let Some(chunk) = cursor.item() { + let overshoot = Unclipped(point.0 - cursor.start()); + *cursor.start() + chunk.clip_point_utf16(overshoot, bias) + } else { + self.summary().lines_utf16() + } + } + + pub fn line_len(&self, row: u32) -> u32 { + self.clip_point(Point::new(row, u32::MAX), Bias::Left) + .column + } + + pub fn fingerprint(&self) -> RopeFingerprint { + self.chunks.summary().fingerprint + } +} + +impl<'a> From<&'a str> for Rope { + fn from(text: &'a str) -> Self { + let mut rope = Self::new(); + rope.push(text); + rope + } +} + +impl<'a> FromIterator<&'a str> for Rope { + fn from_iter>(iter: T) -> Self { + let mut rope = Rope::new(); + for chunk in iter { + rope.push(chunk); + } + rope + } +} + +impl From for Rope { + fn from(text: String) -> Self { + Rope::from(text.as_str()) + } +} + +impl fmt::Display for Rope { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for chunk in self.chunks() { + write!(f, "{}", chunk)?; + } + Ok(()) + } +} + +impl fmt::Debug for Rope { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use std::fmt::Write as _; + + write!(f, "\"")?; + let mut format_string = String::new(); + for chunk in self.chunks() { + write!(&mut format_string, "{:?}", chunk)?; + write!(f, "{}", &format_string[1..format_string.len() - 1])?; + format_string.clear(); + } + write!(f, "\"")?; + Ok(()) + } +} + +pub struct Cursor<'a> { + rope: &'a Rope, + chunks: sum_tree::Cursor<'a, Chunk, usize>, + offset: usize, +} + +impl<'a> Cursor<'a> { + pub fn new(rope: &'a Rope, offset: usize) -> Self { + let mut chunks = rope.chunks.cursor(); + chunks.seek(&offset, Bias::Right, &()); + Self { + rope, + chunks, + offset, + } + } + + pub fn seek_forward(&mut self, end_offset: usize) { + debug_assert!(end_offset >= self.offset); + + self.chunks.seek_forward(&end_offset, Bias::Right, &()); + self.offset = end_offset; + } + + pub fn slice(&mut self, end_offset: usize) -> Rope { + debug_assert!( + end_offset >= self.offset, + "cannot slice backwards from {} to {}", + self.offset, + end_offset + ); + + let mut slice = Rope::new(); + if let Some(start_chunk) = self.chunks.item() { + let start_ix = self.offset - self.chunks.start(); + let end_ix = cmp::min(end_offset, self.chunks.end(&())) - self.chunks.start(); + slice.push(&start_chunk.0[start_ix..end_ix]); + } + + if end_offset > self.chunks.end(&()) { + self.chunks.next(&()); + slice.append(Rope { + chunks: self.chunks.slice(&end_offset, Bias::Right, &()), + }); + if let Some(end_chunk) = self.chunks.item() { + let end_ix = end_offset - self.chunks.start(); + slice.push(&end_chunk.0[..end_ix]); + } + } + + self.offset = end_offset; + slice + } + + pub fn summary(&mut self, end_offset: usize) -> D { + debug_assert!(end_offset >= self.offset); + + let mut summary = D::default(); + if let Some(start_chunk) = self.chunks.item() { + let start_ix = self.offset - self.chunks.start(); + let end_ix = cmp::min(end_offset, self.chunks.end(&())) - self.chunks.start(); + summary.add_assign(&D::from_text_summary(&TextSummary::from( + &start_chunk.0[start_ix..end_ix], + ))); + } + + if end_offset > self.chunks.end(&()) { + self.chunks.next(&()); + summary.add_assign(&self.chunks.summary(&end_offset, Bias::Right, &())); + if let Some(end_chunk) = self.chunks.item() { + let end_ix = end_offset - self.chunks.start(); + summary.add_assign(&D::from_text_summary(&TextSummary::from( + &end_chunk.0[..end_ix], + ))); + } + } + + self.offset = end_offset; + summary + } + + pub fn suffix(mut self) -> Rope { + self.slice(self.rope.chunks.extent(&())) + } + + pub fn offset(&self) -> usize { + self.offset + } +} + +pub struct Chunks<'a> { + chunks: sum_tree::Cursor<'a, Chunk, usize>, + range: Range, + reversed: bool, +} + +impl<'a> Chunks<'a> { + pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { + let mut chunks = rope.chunks.cursor(); + if reversed { + chunks.seek(&range.end, Bias::Left, &()); + } else { + chunks.seek(&range.start, Bias::Right, &()); + } + Self { + chunks, + range, + reversed, + } + } + + pub fn offset(&self) -> usize { + if self.reversed { + self.range.end.min(self.chunks.end(&())) + } else { + self.range.start.max(*self.chunks.start()) + } + } + + pub fn seek(&mut self, offset: usize) { + let bias = if self.reversed { + Bias::Left + } else { + Bias::Right + }; + + if offset >= self.chunks.end(&()) { + self.chunks.seek_forward(&offset, bias, &()); + } else { + self.chunks.seek(&offset, bias, &()); + } + + if self.reversed { + self.range.end = offset; + } else { + self.range.start = offset; + } + } + + pub fn peek(&self) -> Option<&'a str> { + let chunk = self.chunks.item()?; + if self.reversed && self.range.start >= self.chunks.end(&()) { + return None; + } + let chunk_start = *self.chunks.start(); + if self.range.end <= chunk_start { + return None; + } + + let start = self.range.start.saturating_sub(chunk_start); + let end = self.range.end - chunk_start; + Some(&chunk.0[start..chunk.0.len().min(end)]) + } +} + +impl<'a> Iterator for Chunks<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + let result = self.peek(); + if result.is_some() { + if self.reversed { + self.chunks.prev(&()); + } else { + self.chunks.next(&()); + } + } + result + } +} + +pub struct Bytes<'a> { + chunks: sum_tree::Cursor<'a, Chunk, usize>, + range: Range, + reversed: bool, +} + +impl<'a> Bytes<'a> { + pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { + let mut chunks = rope.chunks.cursor(); + if reversed { + chunks.seek(&range.end, Bias::Left, &()); + } else { + chunks.seek(&range.start, Bias::Right, &()); + } + Self { + chunks, + range, + reversed, + } + } + + pub fn peek(&self) -> Option<&'a [u8]> { + let chunk = self.chunks.item()?; + if self.reversed && self.range.start >= self.chunks.end(&()) { + return None; + } + let chunk_start = *self.chunks.start(); + if self.range.end <= chunk_start { + return None; + } + let start = self.range.start.saturating_sub(chunk_start); + let end = self.range.end - chunk_start; + Some(&chunk.0.as_bytes()[start..chunk.0.len().min(end)]) + } +} + +impl<'a> Iterator for Bytes<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option { + let result = self.peek(); + if result.is_some() { + if self.reversed { + self.chunks.prev(&()); + } else { + self.chunks.next(&()); + } + } + result + } +} + +impl<'a> io::Read for Bytes<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if let Some(chunk) = self.peek() { + let len = cmp::min(buf.len(), chunk.len()); + if self.reversed { + buf[..len].copy_from_slice(&chunk[chunk.len() - len..]); + buf[..len].reverse(); + self.range.end -= len; + } else { + buf[..len].copy_from_slice(&chunk[..len]); + self.range.start += len; + } + + if len == chunk.len() { + if self.reversed { + self.chunks.prev(&()); + } else { + self.chunks.next(&()); + } + } + Ok(len) + } else { + Ok(0) + } + } +} + +#[derive(Clone, Debug, Default)] +struct Chunk(ArrayString<{ 2 * CHUNK_BASE }>); + +impl Chunk { + fn offset_to_offset_utf16(&self, target: usize) -> OffsetUtf16 { + let mut offset = 0; + let mut offset_utf16 = OffsetUtf16(0); + for ch in self.0.chars() { + if offset >= target { + break; + } + + offset += ch.len_utf8(); + offset_utf16.0 += ch.len_utf16(); + } + offset_utf16 + } + + fn offset_utf16_to_offset(&self, target: OffsetUtf16) -> usize { + let mut offset_utf16 = OffsetUtf16(0); + let mut offset = 0; + for ch in self.0.chars() { + if offset_utf16 >= target { + break; + } + + offset += ch.len_utf8(); + offset_utf16.0 += ch.len_utf16(); + } + offset + } + + fn offset_to_point(&self, target: usize) -> Point { + let mut offset = 0; + let mut point = Point::new(0, 0); + for ch in self.0.chars() { + if offset >= target { + break; + } + + if ch == '\n' { + point.row += 1; + point.column = 0; + } else { + point.column += ch.len_utf8() as u32; + } + offset += ch.len_utf8(); + } + point + } + + fn offset_to_point_utf16(&self, target: usize) -> PointUtf16 { + let mut offset = 0; + let mut point = PointUtf16::new(0, 0); + for ch in self.0.chars() { + if offset >= target { + break; + } + + if ch == '\n' { + point.row += 1; + point.column = 0; + } else { + point.column += ch.len_utf16() as u32; + } + offset += ch.len_utf8(); + } + point + } + + fn point_to_offset(&self, target: Point) -> usize { + let mut offset = 0; + let mut point = Point::new(0, 0); + + for ch in self.0.chars() { + if point >= target { + if point > target { + debug_panic!("point {target:?} is inside of character {ch:?}"); + } + break; + } + + if ch == '\n' { + point.row += 1; + point.column = 0; + + if point.row > target.row { + debug_panic!( + "point {target:?} is beyond the end of a line with length {}", + point.column + ); + break; + } + } else { + point.column += ch.len_utf8() as u32; + } + + offset += ch.len_utf8(); + } + + offset + } + + fn point_to_point_utf16(&self, target: Point) -> PointUtf16 { + let mut point = Point::zero(); + let mut point_utf16 = PointUtf16::new(0, 0); + for ch in self.0.chars() { + if point >= target { + break; + } + + if ch == '\n' { + point_utf16.row += 1; + point_utf16.column = 0; + point.row += 1; + point.column = 0; + } else { + point_utf16.column += ch.len_utf16() as u32; + point.column += ch.len_utf8() as u32; + } + } + point_utf16 + } + + fn point_utf16_to_offset(&self, target: PointUtf16, clip: bool) -> usize { + let mut offset = 0; + let mut point = PointUtf16::new(0, 0); + + for ch in self.0.chars() { + if point == target { + break; + } + + if ch == '\n' { + point.row += 1; + point.column = 0; + + if point.row > target.row { + if !clip { + debug_panic!( + "point {target:?} is beyond the end of a line with length {}", + point.column + ); + } + // Return the offset of the newline + return offset; + } + } else { + point.column += ch.len_utf16() as u32; + } + + if point > target { + if !clip { + debug_panic!("point {target:?} is inside of codepoint {ch:?}"); + } + // Return the offset of the codepoint which we have landed within, bias left + return offset; + } + + offset += ch.len_utf8(); + } + + offset + } + + fn unclipped_point_utf16_to_point(&self, target: Unclipped) -> Point { + let mut point = Point::zero(); + let mut point_utf16 = PointUtf16::zero(); + + for ch in self.0.chars() { + if point_utf16 == target.0 { + break; + } + + if point_utf16 > target.0 { + // If the point is past the end of a line or inside of a code point, + // return the last valid point before the target. + return point; + } + + if ch == '\n' { + point_utf16 += PointUtf16::new(1, 0); + point += Point::new(1, 0); + } else { + point_utf16 += PointUtf16::new(0, ch.len_utf16() as u32); + point += Point::new(0, ch.len_utf8() as u32); + } + } + + point + } + + fn clip_point(&self, target: Point, bias: Bias) -> Point { + for (row, line) in self.0.split('\n').enumerate() { + if row == target.row as usize { + let mut column = target.column.min(line.len() as u32); + while !line.is_char_boundary(column as usize) { + match bias { + Bias::Left => column -= 1, + Bias::Right => column += 1, + } + } + return Point::new(row as u32, column); + } + } + unreachable!() + } + + fn clip_point_utf16(&self, target: Unclipped, bias: Bias) -> PointUtf16 { + for (row, line) in self.0.split('\n').enumerate() { + if row == target.0.row as usize { + let mut code_units = line.encode_utf16(); + let mut column = code_units.by_ref().take(target.0.column as usize).count(); + if char::decode_utf16(code_units).next().transpose().is_err() { + match bias { + Bias::Left => column -= 1, + Bias::Right => column += 1, + } + } + return PointUtf16::new(row as u32, column as u32); + } + } + unreachable!() + } + + fn clip_offset_utf16(&self, target: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + let mut code_units = self.0.encode_utf16(); + let mut offset = code_units.by_ref().take(target.0 as usize).count(); + if char::decode_utf16(code_units).next().transpose().is_err() { + match bias { + Bias::Left => offset -= 1, + Bias::Right => offset += 1, + } + } + OffsetUtf16(offset) + } +} + +impl sum_tree::Item for Chunk { + type Summary = ChunkSummary; + + fn summary(&self) -> Self::Summary { + ChunkSummary::from(self.0.as_str()) + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct ChunkSummary { + text: TextSummary, + fingerprint: RopeFingerprint, +} + +impl<'a> From<&'a str> for ChunkSummary { + fn from(text: &'a str) -> Self { + Self { + text: TextSummary::from(text), + fingerprint: bromberg_sl2::hash_strict(text.as_bytes()), + } + } +} + +impl sum_tree::Summary for ChunkSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + self.text += &summary.text; + self.fingerprint = self.fingerprint * summary.fingerprint; + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct TextSummary { + pub len: usize, + pub len_utf16: OffsetUtf16, + pub lines: Point, + pub first_line_chars: u32, + pub last_line_chars: u32, + pub last_line_len_utf16: u32, + pub longest_row: u32, + pub longest_row_chars: u32, +} + +impl TextSummary { + pub fn lines_utf16(&self) -> PointUtf16 { + PointUtf16 { + row: self.lines.row, + column: self.last_line_len_utf16, + } + } +} + +impl<'a> From<&'a str> for TextSummary { + fn from(text: &'a str) -> Self { + let mut len_utf16 = OffsetUtf16(0); + let mut lines = Point::new(0, 0); + let mut first_line_chars = 0; + let mut last_line_chars = 0; + let mut last_line_len_utf16 = 0; + let mut longest_row = 0; + let mut longest_row_chars = 0; + for c in text.chars() { + len_utf16.0 += c.len_utf16(); + + if c == '\n' { + lines += Point::new(1, 0); + last_line_len_utf16 = 0; + last_line_chars = 0; + } else { + lines.column += c.len_utf8() as u32; + last_line_len_utf16 += c.len_utf16() as u32; + last_line_chars += 1; + } + + if lines.row == 0 { + first_line_chars = last_line_chars; + } + + if last_line_chars > longest_row_chars { + longest_row = lines.row; + longest_row_chars = last_line_chars; + } + } + + TextSummary { + len: text.len(), + len_utf16, + lines, + first_line_chars, + last_line_chars, + last_line_len_utf16, + longest_row, + longest_row_chars, + } + } +} + +impl sum_tree::Summary for TextSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &Self::Context) { + *self += summary; + } +} + +impl std::ops::Add for TextSummary { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + AddAssign::add_assign(&mut self, &rhs); + self + } +} + +impl<'a> std::ops::AddAssign<&'a Self> for TextSummary { + fn add_assign(&mut self, other: &'a Self) { + let joined_chars = self.last_line_chars + other.first_line_chars; + if joined_chars > self.longest_row_chars { + self.longest_row = self.lines.row; + self.longest_row_chars = joined_chars; + } + if other.longest_row_chars > self.longest_row_chars { + self.longest_row = self.lines.row + other.longest_row; + self.longest_row_chars = other.longest_row_chars; + } + + if self.lines.row == 0 { + self.first_line_chars += other.first_line_chars; + } + + if other.lines.row == 0 { + self.last_line_chars += other.first_line_chars; + self.last_line_len_utf16 += other.last_line_len_utf16; + } else { + self.last_line_chars = other.last_line_chars; + self.last_line_len_utf16 = other.last_line_len_utf16; + } + + self.len += other.len; + self.len_utf16 += other.len_utf16; + self.lines += other.lines; + } +} + +impl std::ops::AddAssign for TextSummary { + fn add_assign(&mut self, other: Self) { + *self += &other; + } +} + +pub trait TextDimension: 'static + for<'a> Dimension<'a, ChunkSummary> { + fn from_text_summary(summary: &TextSummary) -> Self; + fn add_assign(&mut self, other: &Self); +} + +impl TextDimension for (D1, D2) { + fn from_text_summary(summary: &TextSummary) -> Self { + ( + D1::from_text_summary(summary), + D2::from_text_summary(summary), + ) + } + + fn add_assign(&mut self, other: &Self) { + self.0.add_assign(&other.0); + self.1.add_assign(&other.1); + } +} + +impl<'a> sum_tree::Dimension<'a, ChunkSummary> for TextSummary { + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + *self += &summary.text; + } +} + +impl TextDimension for TextSummary { + fn from_text_summary(summary: &TextSummary) -> Self { + summary.clone() + } + + fn add_assign(&mut self, other: &Self) { + *self += other; + } +} + +impl<'a> sum_tree::Dimension<'a, ChunkSummary> for usize { + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + *self += summary.text.len; + } +} + +impl TextDimension for usize { + fn from_text_summary(summary: &TextSummary) -> Self { + summary.len + } + + fn add_assign(&mut self, other: &Self) { + *self += other; + } +} + +impl<'a> sum_tree::Dimension<'a, ChunkSummary> for OffsetUtf16 { + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + *self += summary.text.len_utf16; + } +} + +impl TextDimension for OffsetUtf16 { + fn from_text_summary(summary: &TextSummary) -> Self { + summary.len_utf16 + } + + fn add_assign(&mut self, other: &Self) { + *self += other; + } +} + +impl<'a> sum_tree::Dimension<'a, ChunkSummary> for Point { + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + *self += summary.text.lines; + } +} + +impl TextDimension for Point { + fn from_text_summary(summary: &TextSummary) -> Self { + summary.lines + } + + fn add_assign(&mut self, other: &Self) { + *self += other; + } +} + +impl<'a> sum_tree::Dimension<'a, ChunkSummary> for PointUtf16 { + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + *self += summary.text.lines_utf16(); + } +} + +impl TextDimension for PointUtf16 { + fn from_text_summary(summary: &TextSummary) -> Self { + summary.lines_utf16() + } + + fn add_assign(&mut self, other: &Self) { + *self += other; + } +} + +fn find_split_ix(text: &str) -> usize { + let mut ix = text.len() / 2; + while !text.is_char_boundary(ix) { + if ix < 2 * CHUNK_BASE { + ix += 1; + } else { + ix = (text.len() / 2) - 1; + break; + } + } + while !text.is_char_boundary(ix) { + ix -= 1; + } + + debug_assert!(ix <= 2 * CHUNK_BASE); + debug_assert!(text.len() - ix <= 2 * CHUNK_BASE); + ix +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::prelude::*; + use std::{cmp::Ordering, env, io::Read}; + use util::RandomCharIter; + use Bias::{Left, Right}; + + #[test] + fn test_all_4_byte_chars() { + let mut rope = Rope::new(); + let text = "🏀".repeat(256); + rope.push(&text); + assert_eq!(rope.text(), text); + } + + #[test] + fn test_clip() { + let rope = Rope::from("🧘"); + + assert_eq!(rope.clip_offset(1, Bias::Left), 0); + assert_eq!(rope.clip_offset(1, Bias::Right), 4); + assert_eq!(rope.clip_offset(5, Bias::Right), 4); + + assert_eq!( + rope.clip_point(Point::new(0, 1), Bias::Left), + Point::new(0, 0) + ); + assert_eq!( + rope.clip_point(Point::new(0, 1), Bias::Right), + Point::new(0, 4) + ); + assert_eq!( + rope.clip_point(Point::new(0, 5), Bias::Right), + Point::new(0, 4) + ); + + assert_eq!( + rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Left), + PointUtf16::new(0, 0) + ); + assert_eq!( + rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Right), + PointUtf16::new(0, 2) + ); + assert_eq!( + rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 3)), Bias::Right), + PointUtf16::new(0, 2) + ); + + assert_eq!( + rope.clip_offset_utf16(OffsetUtf16(1), Bias::Left), + OffsetUtf16(0) + ); + assert_eq!( + rope.clip_offset_utf16(OffsetUtf16(1), Bias::Right), + OffsetUtf16(2) + ); + assert_eq!( + rope.clip_offset_utf16(OffsetUtf16(3), Bias::Right), + OffsetUtf16(2) + ); + } + + #[gpui::test(iterations = 100)] + fn test_random_rope(mut rng: StdRng) { + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let mut expected = String::new(); + let mut actual = Rope::new(); + for _ in 0..operations { + let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right); + let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left); + let len = rng.gen_range(0..=64); + let new_text: String = RandomCharIter::new(&mut rng).take(len).collect(); + + let mut new_actual = Rope::new(); + let mut cursor = actual.cursor(0); + new_actual.append(cursor.slice(start_ix)); + new_actual.push(&new_text); + cursor.seek_forward(end_ix); + new_actual.append(cursor.suffix()); + actual = new_actual; + + expected.replace_range(start_ix..end_ix, &new_text); + + assert_eq!(actual.text(), expected); + log::info!("text: {:?}", expected); + + for _ in 0..5 { + let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right); + let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left); + + let actual_text = actual.chunks_in_range(start_ix..end_ix).collect::(); + assert_eq!(actual_text, &expected[start_ix..end_ix]); + + let mut actual_text = String::new(); + actual + .bytes_in_range(start_ix..end_ix) + .read_to_string(&mut actual_text) + .unwrap(); + assert_eq!(actual_text, &expected[start_ix..end_ix]); + + assert_eq!( + actual + .reversed_chunks_in_range(start_ix..end_ix) + .collect::>() + .into_iter() + .rev() + .collect::(), + &expected[start_ix..end_ix] + ); + } + + let mut offset_utf16 = OffsetUtf16(0); + let mut point = Point::new(0, 0); + let mut point_utf16 = PointUtf16::new(0, 0); + for (ix, ch) in expected.char_indices().chain(Some((expected.len(), '\0'))) { + assert_eq!(actual.offset_to_point(ix), point, "offset_to_point({})", ix); + assert_eq!( + actual.offset_to_point_utf16(ix), + point_utf16, + "offset_to_point_utf16({})", + ix + ); + assert_eq!( + actual.point_to_offset(point), + ix, + "point_to_offset({:?})", + point + ); + assert_eq!( + actual.point_utf16_to_offset(point_utf16), + ix, + "point_utf16_to_offset({:?})", + point_utf16 + ); + assert_eq!( + actual.offset_to_offset_utf16(ix), + offset_utf16, + "offset_to_offset_utf16({:?})", + ix + ); + assert_eq!( + actual.offset_utf16_to_offset(offset_utf16), + ix, + "offset_utf16_to_offset({:?})", + offset_utf16 + ); + if ch == '\n' { + point += Point::new(1, 0); + point_utf16 += PointUtf16::new(1, 0); + } else { + point.column += ch.len_utf8() as u32; + point_utf16.column += ch.len_utf16() as u32; + } + offset_utf16.0 += ch.len_utf16(); + } + + let mut offset_utf16 = OffsetUtf16(0); + let mut point_utf16 = Unclipped(PointUtf16::zero()); + for unit in expected.encode_utf16() { + let left_offset = actual.clip_offset_utf16(offset_utf16, Bias::Left); + let right_offset = actual.clip_offset_utf16(offset_utf16, Bias::Right); + assert!(right_offset >= left_offset); + // Ensure translating UTF-16 offsets to UTF-8 offsets doesn't panic. + actual.offset_utf16_to_offset(left_offset); + actual.offset_utf16_to_offset(right_offset); + + let left_point = actual.clip_point_utf16(point_utf16, Bias::Left); + let right_point = actual.clip_point_utf16(point_utf16, Bias::Right); + assert!(right_point >= left_point); + // Ensure translating valid UTF-16 points to offsets doesn't panic. + actual.point_utf16_to_offset(left_point); + actual.point_utf16_to_offset(right_point); + + offset_utf16.0 += 1; + if unit == b'\n' as u16 { + point_utf16.0 += PointUtf16::new(1, 0); + } else { + point_utf16.0 += PointUtf16::new(0, 1); + } + } + + for _ in 0..5 { + let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right); + let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left); + assert_eq!( + actual.cursor(start_ix).summary::(end_ix), + TextSummary::from(&expected[start_ix..end_ix]) + ); + } + + let mut expected_longest_rows = Vec::new(); + let mut longest_line_len = -1_isize; + for (row, line) in expected.split('\n').enumerate() { + let row = row as u32; + assert_eq!( + actual.line_len(row), + line.len() as u32, + "invalid line len for row {}", + row + ); + + let line_char_count = line.chars().count() as isize; + match line_char_count.cmp(&longest_line_len) { + Ordering::Less => {} + Ordering::Equal => expected_longest_rows.push(row), + Ordering::Greater => { + longest_line_len = line_char_count; + expected_longest_rows.clear(); + expected_longest_rows.push(row); + } + } + } + + let longest_row = actual.summary().longest_row; + assert!( + expected_longest_rows.contains(&longest_row), + "incorrect longest row {}. expected {:?} with length {}", + longest_row, + expected_longest_rows, + longest_line_len, + ); + } + } + + fn clip_offset(text: &str, mut offset: usize, bias: Bias) -> usize { + while !text.is_char_boundary(offset) { + match bias { + Bias::Left => offset -= 1, + Bias::Right => offset += 1, + } + } + offset + } + + impl Rope { + fn text(&self) -> String { + let mut text = String::new(); + for chunk in self.chunks.cursor::<()>() { + text.push_str(&chunk.0); + } + text + } + } +} diff --git a/crates/rope2/src/unclipped.rs b/crates/rope2/src/unclipped.rs new file mode 100644 index 0000000000..937cbca053 --- /dev/null +++ b/crates/rope2/src/unclipped.rs @@ -0,0 +1,57 @@ +use crate::{ChunkSummary, TextDimension, TextSummary}; +use std::ops::{Add, AddAssign, Sub, SubAssign}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Unclipped(pub T); + +impl From for Unclipped { + fn from(value: T) -> Self { + Unclipped(value) + } +} + +impl<'a, T: sum_tree::Dimension<'a, ChunkSummary>> sum_tree::Dimension<'a, ChunkSummary> + for Unclipped +{ + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + self.0.add_summary(summary, &()); + } +} + +impl TextDimension for Unclipped { + fn from_text_summary(summary: &TextSummary) -> Self { + Unclipped(T::from_text_summary(summary)) + } + + fn add_assign(&mut self, other: &Self) { + TextDimension::add_assign(&mut self.0, &other.0); + } +} + +impl> Add> for Unclipped { + type Output = Unclipped; + + fn add(self, rhs: Unclipped) -> Self::Output { + Unclipped(self.0 + rhs.0) + } +} + +impl> Sub> for Unclipped { + type Output = Unclipped; + + fn sub(self, rhs: Unclipped) -> Self::Output { + Unclipped(self.0 - rhs.0) + } +} + +impl> AddAssign> for Unclipped { + fn add_assign(&mut self, rhs: Unclipped) { + self.0 += rhs.0; + } +} + +impl> SubAssign> for Unclipped { + fn sub_assign(&mut self, rhs: Unclipped) { + self.0 -= rhs.0; + } +} diff --git a/crates/text2/Cargo.toml b/crates/text2/Cargo.toml index 7c12d22adf..679a2df8d6 100644 --- a/crates/text2/Cargo.toml +++ b/crates/text2/Cargo.toml @@ -14,7 +14,7 @@ test-support = ["rand"] [dependencies] clock = { path = "../clock" } collections = { path = "../collections" } -rope = { path = "../rope" } +rope = { package = "rope2", path = "../rope2" } sum_tree = { path = "../sum_tree" } util = { path = "../util" } diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index d510de24ee..8cc8cdcec9 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -58,6 +58,7 @@ project = { package = "project2", path = "../project2" } # project_symbols = { path = "../project_symbols" } # quick_action_bar = { path = "../quick_action_bar" } # recent_projects = { path = "../recent_projects" } +rope = { package = "rope2", path = "../rope2"} rpc = { package = "rpc2", path = "../rpc2" } settings = { package = "settings2", path = "../settings2" } feature_flags = { package = "feature_flags2", path = "../feature_flags2" }