diff --git a/src/rope/rope.rs b/src/rope/rope.rs index 27c767b..985a5d4 100644 --- a/src/rope/rope.rs +++ b/src/rope/rope.rs @@ -34,6 +34,10 @@ pub struct Rope { } impl Rope { + pub(super) const fn arity() -> usize { + ARITY + } + #[doc(hidden)] pub fn assert_invariants(&self) { self.tree.assert_invariants(); @@ -66,6 +70,8 @@ impl Rope { /// Returns the byte at `byte_index`. /// + /// For a checked version, see [`get_byte`](Self::get_byte). + /// /// # Panics /// /// Panics if the byte index is out of bounds (i.e. greater than or equal @@ -85,14 +91,9 @@ impl Rope { #[track_caller] #[inline] pub fn byte(&self, byte_index: usize) -> u8 { - if byte_index >= self.byte_len() { - panic::byte_index_out_of_bounds(byte_index, self.byte_len()); - } - - let (chunk, ByteMetric(chunk_byte_offset)) = - self.tree.leaf_at_measure(ByteMetric(byte_index + 1)); - - chunk.byte(byte_index - chunk_byte_offset) + self.get_byte(byte_index).unwrap_or_else(|| { + panic::byte_index_out_of_bounds(byte_index, self.line_len()) + }) } /// Returns the length of the `Rope` in bytes. @@ -299,8 +300,120 @@ impl Rope { self.replace(byte_range, ""); } - pub(super) const fn arity() -> usize { - ARITY + /// Returns the byte at `byte_index`, if it exists. + /// + /// If `byte_index` is out of bounds, returns `None`. + /// + /// For a panic'ing version, see [`byte`](Self::byte). + /// + /// # Examples + /// + /// ``` + /// # use crop::Rope; + /// # + /// let r = Rope::from("bar"); + /// + /// assert_eq!(r.get_byte(0), Some(b'b')); + /// assert_eq!(r.get_byte(1), Some(b'a')); + /// assert_eq!(r.get_byte(2), Some(b'r')); + /// assert_eq!(r.get_byte(3), None); + /// ``` + #[track_caller] + #[inline] + pub fn get_byte(&self, byte_index: usize) -> Option { + if byte_index >= self.byte_len() { + return None; + } + + let (chunk, ByteMetric(chunk_byte_offset)) = + self.tree.leaf_at_measure(ByteMetric(byte_index + 1)); + + Some(chunk.byte(byte_index - chunk_byte_offset)) + } + + /// Returns the line at `line_index`, without its line terminator, if it exists. + /// + /// If `line_index` is out of bounds, returns `None`. + /// + /// If you want to include the line break consider calling + /// [`get_line_slice()`](Self::get_line_slice()) in the + /// `line_index..line_index + 1` range. + /// + /// For a panic'ing version, see [`line_slice()`](Self::line_slice()). + /// + /// # Examples + /// + /// ``` + /// # use crop::Rope; + /// # + /// let r = Rope::from("foo\nbar\r\nbaz"); + /// + /// assert_eq!(r.get_line(0).unwrap(), "foo"); + /// assert_eq!(r.get_line(1).unwrap(), "bar"); + /// assert_eq!(r.get_line(2).unwrap(), "baz"); + /// assert_eq!(r.get_line(3), None); + /// ``` + #[track_caller] + #[inline] + pub fn get_line(&self, line_index: usize) -> Option> { + if line_index >= self.line_len() { + return None; + } + + let tree_slice = self + .tree + .slice(RawLineMetric(line_index)..RawLineMetric(line_index + 1)); + + let mut line = RopeSlice { tree_slice, has_trailing_newline: false }; + + if line.tree_slice.summary().line_breaks() == 1 { + line.truncate_trailing_line_break(); + } + + Some(line) + } + + /// Returns an immutable slice of the `Rope` in the specified line range, + /// if it exists, where the start and end of the range are interpreted as + /// offsets. + /// + /// If `line_range` is out of bounds, returns `None`. + /// + /// If you want a single line, see [`get_line()`](Self::get_line). + /// + /// For a panic'ing version, see [`line_slice`](Self::line_slice). + /// + /// # Examples + /// + /// ``` + /// # use crop::Rope; + /// # + /// let r = Rope::from("foo\nbar\r\nbaz\nfoobar\n"); + /// + /// assert_eq!(r.get_line_slice(..1).unwrap(), "foo\n"); + /// assert_eq!(r.get_line_slice(1..3).unwrap(), "bar\r\nbaz\n"); + /// assert_eq!(r.get_line_slice(3..).unwrap(), "foobar\n"); + /// assert_eq!(r.get_line_slice(4..).unwrap(), ""); + /// assert_eq!(r.get_line_slice(5..), None); + /// ``` + #[track_caller] + #[inline] + pub fn get_line_slice(&self, line_range: R) -> Option> + where + R: RangeBounds, + { + let (start, end) = + range_bounds_to_start_end(line_range, 0, self.line_len()); + + if start > end { + return None; + } + + if end > self.line_len() { + return None; + } + + Some(self.tree.slice(RawLineMetric(start)..RawLineMetric(end)).into()) } /// Returns an iterator over the extended grapheme clusters of this @@ -446,6 +559,9 @@ impl Rope { /// [`line_slice()`](Self::line_slice()) in the /// `line_index..line_index + 1` range. /// + /// For a checked version, see + /// [`get_line_slice()`](Self::get_line_slice()). + /// /// # Panics /// /// Panics if the line index is out of bounds (i.e. greater than or equal @@ -465,21 +581,9 @@ impl Rope { #[track_caller] #[inline] pub fn line(&self, line_index: usize) -> RopeSlice<'_> { - if line_index >= self.line_len() { - panic::line_index_out_of_bounds(line_index, self.line_len()); - } - - let tree_slice = self - .tree - .slice(RawLineMetric(line_index)..RawLineMetric(line_index + 1)); - - let mut line = RopeSlice { tree_slice, has_trailing_newline: false }; - - if line.tree_slice.summary().line_breaks() == 1 { - line.truncate_trailing_line_break(); - } - - line + self.get_line(line_index).unwrap_or_else(|| { + panic::line_index_out_of_bounds(line_index, self.line_len()) + }) } /// Returns the number of lines in the `Rope`. @@ -551,6 +655,10 @@ impl Rope { /// Returns an immutable slice of the `Rope` in the specified line range, /// where the start and end of the range are interpreted as offsets. /// + /// If you want a single line, see [`line()`](Self::line). + /// + /// If you want a checked version, see [`get_line_slice()`](Self::get_line_slice). + /// /// # Panics /// /// Panics if the start is greater than the end or if the end is out of @@ -566,6 +674,7 @@ impl Rope { /// assert_eq!(r.line_slice(..1), "foo\n"); /// assert_eq!(r.line_slice(1..3), "bar\r\nbaz\n"); /// assert_eq!(r.line_slice(3..), "foobar\n"); + /// assert_eq!(r.line_slice(4..), ""); /// ``` #[track_caller] #[inline] diff --git a/src/rope/rope_slice.rs b/src/rope/rope_slice.rs index 0ba7e38..03b5aec 100644 --- a/src/rope/rope_slice.rs +++ b/src/rope/rope_slice.rs @@ -31,6 +31,8 @@ impl<'a> RopeSlice<'a> { /// Returns the byte at `byte_index`. /// + /// For a checked version, see [`get_byte`](Self::get_byte). + /// /// # Panics /// /// Panics if the byte index is out of bounds (i.e. greater than or equal @@ -51,16 +53,10 @@ impl<'a> RopeSlice<'a> { #[track_caller] #[inline] pub fn byte(&self, byte_index: usize) -> u8 { - if byte_index >= self.byte_len() { - panic::byte_index_out_of_bounds(byte_index, self.byte_len()); - } - - let (chunk, ByteMetric(chunk_byte_offset)) = - self.tree_slice.leaf_at_measure(ByteMetric(byte_index + 1)); - - chunk.byte(byte_index - chunk_byte_offset) + self.get_byte(byte_index).unwrap_or_else(|| { + panic::byte_index_out_of_bounds(byte_index, self.line_len()) + }) } - /// Returns the length of the `RopeSlice` in bytes. /// /// # Examples @@ -247,6 +243,129 @@ impl<'a> RopeSlice<'a> { Chunks::from(self) } + /// Returns the byte at `byte_index`, if it exists. + /// + /// If `byte_index` is out of bounds, returns `None`. + /// + /// For a panic'ing version, see [`byte`](Self::byte). + /// + /// # Examples + /// + /// ``` + /// # use crop::Rope; + /// # + /// let r = Rope::from("bar"); + /// let s = r.byte_slice(..); + /// + /// assert_eq!(s.get_byte(0), Some(b'b')); + /// assert_eq!(s.get_byte(1), Some(b'a')); + /// assert_eq!(s.get_byte(2), Some(b'r')); + /// assert_eq!(s.get_byte(3), None); + /// ``` + #[track_caller] + #[inline] + pub fn get_byte(&self, byte_index: usize) -> Option { + if byte_index >= self.byte_len() { + return None; + } + + let (chunk, ByteMetric(chunk_byte_offset)) = + self.tree_slice.leaf_at_measure(ByteMetric(byte_index + 1)); + + Some(chunk.byte(byte_index - chunk_byte_offset)) + } + + /// Returns the line at `line_index`, without its line terminator, if it exists. + /// + /// If `line_index` is out of bounds, returns `None`. + /// + /// If you want to include the line break consider calling + /// [`get_line_slice()`](Self::get_line_slice()) in the + /// `line_index..line_index + 1` range. + /// + /// For a panic'ing version, see [`line_slice()`](Self::line_slice()). + /// + /// # Examples + /// + /// ``` + /// # use crop::Rope; + /// # + /// let r = Rope::from("foo\nbar\r\nbaz"); + /// let s = r.byte_slice(..); + /// + /// assert_eq!(s.get_line(0).unwrap(), "foo"); + /// assert_eq!(s.get_line(1).unwrap(), "bar"); + /// assert_eq!(s.get_line(2).unwrap(), "baz"); + /// assert_eq!(s.get_line(3), None); + /// ``` + #[track_caller] + #[inline] + pub fn get_line(&self, line_index: usize) -> Option> { + if line_index >= self.line_len() { + return None; + } + + let tree_slice = self + .tree_slice + .slice(RawLineMetric(line_index)..RawLineMetric(line_index + 1)); + + let mut line = RopeSlice { tree_slice, has_trailing_newline: false }; + + if line.tree_slice.summary().line_breaks() == 1 { + line.truncate_trailing_line_break(); + } + + Some(line) + } + + /// Returns an immutable slice of the `Rope` in the specified line range, + /// if it exists, where the start and end of the range are interpreted as + /// offsets. + /// + /// If `line_range` is out of bounds, returns `None`. + /// + /// If you want a single line, see [`get_line()`](Self::get_line). + /// + /// For a panic'ing version, see [`line_slice`](Self::line_slice). + /// + /// # Examples + /// + /// ``` + /// # use crop::Rope; + /// # + /// let r = Rope::from("foo\nbar\r\nbaz\nfoobar\n"); + /// let s = r.byte_slice(..); + /// + /// assert_eq!(s.get_line_slice(..1).unwrap(), "foo\n"); + /// assert_eq!(s.get_line_slice(1..3).unwrap(), "bar\r\nbaz\n"); + /// assert_eq!(s.get_line_slice(3..).unwrap(), "foobar\n"); + /// assert_eq!(s.get_line_slice(4..).unwrap(), ""); + /// assert_eq!(s.get_line_slice(5..), None); + /// ``` + #[track_caller] + #[inline] + pub fn get_line_slice(&self, line_range: R) -> Option> + where + R: RangeBounds, + { + let (start, end) = + range_bounds_to_start_end(line_range, 0, self.line_len()); + + if start > end { + return None; + } + + if end > self.line_len() { + return None; + } + + Some( + self.tree_slice + .slice(RawLineMetric(start)..RawLineMetric(end)) + .into(), + ) + } + /// Returns an iterator over the extended grapheme clusters of this /// `RopeSlice`. /// @@ -361,6 +480,9 @@ impl<'a> RopeSlice<'a> { /// [`line_slice()`](Self::line_slice()) in the /// `line_index..line_index + 1` range. /// + /// For a checked version, see + /// [`get_line_slice()`](Self::get_line_slice()). + /// /// # Panics /// /// Panics if the line index is out of bounds (i.e. greater than or equal @@ -379,22 +501,10 @@ impl<'a> RopeSlice<'a> { /// ``` #[track_caller] #[inline] - pub fn line(self, line_index: usize) -> RopeSlice<'a> { - if line_index >= self.line_len() { - panic::line_offset_out_of_bounds(line_index, self.line_len()); - } - - let tree_slice = self - .tree_slice - .slice(RawLineMetric(line_index)..RawLineMetric(line_index + 1)); - - let mut line = Self { tree_slice, has_trailing_newline: false }; - - if line.tree_slice.summary().line_breaks() == 1 { - line.truncate_trailing_line_break(); - } - - line + pub fn line(&self, line_index: usize) -> RopeSlice<'a> { + self.get_line(line_index).unwrap_or_else(|| { + panic::line_index_out_of_bounds(line_index, self.line_len()) + }) } /// Returns the number of lines in the `RopeSlice`. @@ -470,6 +580,10 @@ impl<'a> RopeSlice<'a> { /// Returns a sub-slice of this `RopeSlice` in the specified line range, /// where the start and end of the range are interpreted as offsets. /// + /// If you want a single line, see [`line()`](Self::line). + /// + /// If you want a checked version, see [`get_line_slice()`](Self::get_line_slice). + /// /// # Panics /// /// Panics if the start is greater than the end or if the end is out of