use anyhow::{anyhow, Result}; use itertools::Itertools; use string_offset::{ByteOffset, CharCounter, CharOffset}; use crate::event::ModifiersState; use self::point::Point; use self::word_boundaries::WordBoundaries; pub mod header; pub mod point; pub mod word_boundaries; pub mod words; pub use header::BlockHeaderSize; #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] pub enum SelectionType { #[default] Simple, Semantic, Lines, Rect, } impl SelectionType { pub fn from_click_count(click_count: u32) -> Self { match click_count { 0 => SelectionType::Simple, 1 => SelectionType::Simple, 2 => SelectionType::Semantic, 3 => SelectionType::Lines, _ => SelectionType::Lines, } } pub fn from_mouse_event(modifiers: ModifiersState, click_count: u32) -> Self { let is_rect = if cfg!(target_os = "macos") { modifiers.cmd && modifiers.alt } else { modifiers.ctrl && modifiers.alt }; if is_rect { return SelectionType::Rect; } SelectionType::from_click_count(click_count) } } impl From for IsRect { fn from(selection_type: SelectionType) -> Self { match selection_type { SelectionType::Rect => IsRect::True, _ => IsRect::False, } } } #[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] pub enum IsRect { True, #[default] False, } #[derive(Copy, Clone, Debug, Default)] pub enum SelectionDirection { #[default] Forward, Backward, } /// A buffer of text characters. This trait acts as a base layer to implement text segmentation /// on top of. Currently, it supports word navigation. pub trait TextBuffer { type Chars<'a>: Iterator + 'a where Self: 'a; type CharsReverse<'a>: Iterator + 'a where Self: 'a; /// Begin iterating over the characters at `offset`, continuing to the end of the buffer. /// /// The maximum valid `offset` is the length of the buffer (not 1 less than the length). This /// allows starting just after the last character. fn chars_at(&self, offset: CharOffset) -> Result>; /// Begin iterating backwards over the characters from `offset` to the start of the buffer. /// /// Note that this is _different_ from the semantics of `Iterator::rev`, which would instead /// start at the very end of the buffer. /// /// The maximum valid `offset` is the length of the buffer (not 1 less than the length). This /// allows starting just after the last character. fn chars_rev_at(&self, offset: CharOffset) -> Result>; /// Converts a character offset to a buffer [`Point`], if it is in bounds. fn to_point(&self, offset: CharOffset) -> Result; /// Convert a point to its offset within the buffer. fn to_offset(&self, point: Point) -> Result; /// Get an iterator of word starting points forward from the given offset fn word_starts_from_offset( &self, position: T, ) -> Result> { let offset = position.to_char_offset(self)?; Ok(WordBoundaries::forward_starts( offset, self.chars_at(offset)?, self, )) } /// Get an iterator of word ending points forward from the given offset, excluding the current /// location if it is a word boundary. /// /// Example: For a buffer of "word one two three", with an offset of `4` (immediately after /// the 'word'), this will yield columns [8, 12, 18], the ends of `one`, `two`, and `three`, /// but _excluding_ the initial position at the end of `word`. fn word_ends_from_offset_exclusive( &self, position: T, ) -> Result> { let offset = position.to_char_offset(self)?; Ok(WordBoundaries::forward_ends_exclusive( offset, self.chars_at(offset)?, self, )) } /// Get an iterator of word ending points forward from the given offset, including the current /// location if appropriate. /// /// Example: For a buffer of "word one two three", with an offset of `4` (immediately after /// the 'word'), this will yield columns [4, 8, 12, 18], the ends of all four words, /// _including_ the initial position at the end of `word`. fn word_ends_from_offset_inclusive( &self, position: T, ) -> Result> { let offset = position.to_char_offset(self)?; Ok(WordBoundaries::forward_ends_inclusive( offset, self.chars_at(offset)?, self, )) } /// Get an iterator of word starting points backwards from the given offset, excluding the /// current location if it is a word boundary. /// /// Example: For a buffer of "word one two three", with an offset of `13` (immediately before /// the 'three'), this will yield columns [9, 5, 0], the starts of `two`, `one`, and `word`, /// but _excluding_ the initial position at the start of `three`. fn word_starts_backward_from_offset_exclusive( &self, position: T, ) -> Result> { let offset = position.to_char_offset(self)?; Ok(WordBoundaries::backward_starts_exclusive( offset, self.chars_rev_at(offset)?, self, )) } /// Get an iterator of word starting points backwards from the given offset, including the /// current location if appropriate. /// /// Example: For a buffer of "word one two three", with an offset of `13` (immediately before /// the 'three'), this will yield columns [13, 9, 5, 0], the starts of all four words, /// _including_ the initial position at the start of `three`. fn word_starts_backward_from_offset_inclusive( &self, position: T, ) -> Result> { let offset = position.to_char_offset(self)?; Ok(WordBoundaries::backward_starts_inclusive( offset, self.chars_rev_at(offset)?, self, )) } } /// A type which can index into a text buffer. pub trait BufferIndex { fn to_char_offset(&self, buffer: &B) -> Result; } impl BufferIndex for CharOffset { fn to_char_offset(&self, _: &B) -> Result { Ok(*self) } } impl BufferIndex for Point { fn to_char_offset(&self, buffer: &B) -> Result { buffer.to_offset(*self) } } impl TextBuffer for str { type Chars<'a> = std::str::Chars<'a>; type CharsReverse<'a> = std::iter::Rev>; fn chars_at(&self, offset: CharOffset) -> Result> { let chars = self.chars().count(); if offset.as_usize() <= chars { Ok(self.chars().dropping(offset.as_usize())) } else { Err(anyhow!( "Offset {offset} out of bounds; char length is {chars}" )) } } fn chars_rev_at(&self, offset: CharOffset) -> Result> { let chars = self.chars().count(); if offset.as_usize() <= chars { Ok(self.chars().rev().dropping(chars - offset.as_usize())) } else { Err(anyhow!( "Offset {offset} out of bounds; char length is {chars}" )) } } fn to_point(&self, offset: CharOffset) -> Result { let chars = self.chars().count(); if offset.as_usize() <= chars { Ok(Point::new(0, offset.as_usize() as u32)) } else { Err(anyhow!( "Offset {offset} out of bounds; char length is {chars}" )) } } fn to_offset(&self, point: Point) -> Result { if point.row == 0 { let chars = self.chars().count(); if (point.column as usize) <= chars { Ok(CharOffset::from(point.column as usize)) } else { Err(anyhow!( "Column {} out of bounds; char length is {chars}", point.column )) } } else { Err(anyhow!( "Row {} out of bounds; str only has 1 row", point.row )) } } } /// Convert a slice of text into a `Vec` of UTF-8 bytes. pub fn str_to_byte_vec(text: &str) -> Vec { text.as_bytes().iter().cloned().collect_vec() } /// Slice a string by [`char`] offsets, rather than byte offsets. /// /// The starting index is inclusive, while the ending index is exclusive. pub fn char_slice(s: &str, start: usize, end: usize) -> Option<&str> { if end < start { return None; } if start == end { return Some(""); } let mut indices = s.char_indices(); let (start_index, _) = indices.nth(start)?; // Why not just use `nth()` again? We need to distinguish between a `None` because `end` // is out of bounds and a `None` because `end` is the end of the string. // If/when Iterator::advance_by (https://github.com/rust-lang/rust/issues/77404) stabilizes, // we should use that. In the meantime, this doesn't hurt performance because `nth()` // also has to advance character-by-character. for _ in start + 1..end { indices.next()?; } let end_index = match indices.next() { Some((index, _)) => index, None => s.len(), }; s.get(start_index..end_index) } pub fn count_chars_up_to_byte(text: &str, byte_offset: ByteOffset) -> Option { if byte_offset.as_usize() == text.len() { return Some(CharOffset::from(text.chars().count())); } let mut counter = CharCounter::new(text); counter.char_offset(byte_offset) } #[cfg(test)] #[path = "mod_tests.rs"] mod tests;