use ropey::{iter::Chunks, str_utils, Rope, RopeSlice};
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr;
pub type ByteIndex = usize;
pub type CharIndex = usize;
pub type LineIndex = usize;
pub fn width(tab_width: usize, slice: &RopeSlice) -> usize {
rope_slice_as_str(slice, |text| {
if text == "\t" {
tab_width
} else {
text.chars().filter(|character| *character == '\t').count() * tab_width
+ UnicodeWidthStr::width(text)
}
})
}
pub fn rope_slice_as_str<T>(slice: &RopeSlice, closure: impl FnOnce(&str) -> T) -> T {
if let Some(text) = slice.as_str() {
closure(text)
} else {
let text = slice.chars().collect::<String>();
closure(text.as_str())
}
}
pub struct RopeGrapheme<'a> {
pub slice: RopeSlice<'a>,
pub byte_start: usize,
pub byte_end: usize,
}
impl<'a> std::ops::Deref for RopeGrapheme<'a> {
type Target = RopeSlice<'a>;
fn deref(&self) -> &Self::Target {
&self.slice
}
}
pub struct RopeGraphemes<'a> {
text: RopeSlice<'a>,
chunks: Chunks<'a>,
chunk: &'a str,
chunk_byte_start: usize,
previous_chunk: &'a str,
previous_chunk_byte_start: usize,
pub cursor: GraphemeCursor,
}
impl<'a> RopeGraphemes<'a> {
pub fn new<'b>(slice: &RopeSlice<'b>) -> RopeGraphemes<'b> {
let mut chunks = slice.chunks();
let chunk = chunks.next().unwrap_or("");
RopeGraphemes {
text: *slice,
chunks,
chunk,
chunk_byte_start: 0,
previous_chunk: "",
previous_chunk_byte_start: 0,
cursor: GraphemeCursor::new(0, slice.len_bytes(), true),
}
}
}
impl<'a> Iterator for RopeGraphemes<'a> {
type Item = RopeGrapheme<'a>;
fn next(&mut self) -> Option<RopeGrapheme<'a>> {
let byte_start = self.cursor.cur_cursor();
let byte_end;
loop {
match self.cursor.next_boundary(self.chunk, self.chunk_byte_start) {
Ok(None) => {
return None;
}
Ok(Some(n)) => {
byte_end = n;
break;
}
Err(GraphemeIncomplete::NextChunk) => {
self.previous_chunk = self.chunk;
self.previous_chunk_byte_start = self.chunk_byte_start;
self.chunk_byte_start += self.chunk.len();
self.chunk = self.chunks.next().unwrap_or("");
}
Err(GraphemeIncomplete::PreContext(context_length)) => {
assert!(context_length <= self.previous_chunk.len());
self.cursor
.provide_context(self.previous_chunk, self.previous_chunk_byte_start);
}
Err(error) => {
panic!(
"unexpectedly encountered `{:?}` while iterating over grapheme clusters",
error
);
}
}
}
let slice = if byte_start < self.chunk_byte_start {
let char_start = self.text.byte_to_char(byte_start);
let char_end = self.text.byte_to_char(byte_end);
self.text.slice(char_start..char_end)
} else {
let chunk_byte_start = byte_start - self.chunk_byte_start;
let chunk_byte_end = byte_end - self.chunk_byte_start;
self.chunk[chunk_byte_start..chunk_byte_end].into()
};
Some(RopeGrapheme {
slice,
byte_start,
byte_end,
})
}
}
pub fn strip_trailing_whitespace(mut text: Rope) -> Rope {
let mut trailing_empty_line = true;
for line_index in (0..text.len_lines()).rev() {
let start = text.line_to_char(line_index);
let end = if line_index + 1 < text.len_lines() {
text.line_to_char(line_index + 1)
} else {
text.len_chars()
};
if start == end {
continue;
}
let mut cursor = end - 1;
while cursor > start {
cursor -= 1;
let character = text.char(cursor);
if character.is_whitespace() {
text.remove(cursor..=cursor);
} else {
trailing_empty_line = false;
break;
}
}
if trailing_empty_line && cursor == start {
text.remove(start..text.len_chars());
}
}
if text.len_chars() > 1 && text.char(text.len_chars() - 1) != '\n' {
text.insert_char(text.len_chars(), '\n');
}
text
}
pub trait RopeExt {
fn prev_grapheme_boundary_n(&self, char_index: CharIndex, n: usize) -> CharIndex;
fn next_grapheme_boundary_n(&self, char_index: CharIndex, n: usize) -> CharIndex;
fn prev_grapheme_boundary(&self, char_index: CharIndex) -> usize {
self.prev_grapheme_boundary_n(char_index, 1)
}
fn next_grapheme_boundary(&self, char_index: CharIndex) -> CharIndex {
self.next_grapheme_boundary_n(char_index, 1)
}
}
impl RopeExt for Rope {
fn prev_grapheme_boundary_n(&self, char_index: CharIndex, n: usize) -> CharIndex {
prev_grapheme_boundary_n(self.slice(..), char_index, n)
}
fn next_grapheme_boundary_n(&self, char_index: CharIndex, n: usize) -> CharIndex {
next_grapheme_boundary_n(self.slice(..), char_index, n)
}
}
fn prev_grapheme_boundary_n(slice: RopeSlice, char_index: CharIndex, n: usize) -> CharIndex {
debug_assert!(char_index <= slice.len_chars());
let mut byte_index = slice.char_to_byte(char_index);
let (mut chunk, mut chunk_byte_index, mut chunk_char_index, _) =
slice.chunk_at_byte(byte_index);
let mut gc = GraphemeCursor::new(byte_index, slice.len_bytes(), true);
for _ in 0..n {
loop {
match gc.prev_boundary(chunk, chunk_byte_index) {
Ok(None) => return 0,
Ok(Some(boundry_offset)) => {
byte_index = boundry_offset;
break;
}
Err(GraphemeIncomplete::PrevChunk) => {
let (a, b, c, _) = slice.chunk_at_byte(chunk_byte_index - 1);
chunk = a;
chunk_byte_index = b;
chunk_char_index = c;
}
Err(GraphemeIncomplete::PreContext(offset)) => {
let ctx_chunk = slice.chunk_at_byte(offset - 1).0;
gc.provide_context(ctx_chunk, offset - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
let tmp = str_utils::byte_to_char_idx(chunk, byte_index - chunk_byte_index);
chunk_char_index + tmp
}
fn next_grapheme_boundary_n(slice: RopeSlice, char_index: CharIndex, n: usize) -> CharIndex {
debug_assert!(char_index <= slice.len_chars());
let mut byte_index = slice.char_to_byte(char_index);
let (mut chunk, mut chunk_byte_index, mut chunk_char_index, _) =
slice.chunk_at_byte(byte_index);
let mut cursor = GraphemeCursor::new(byte_index, slice.len_bytes(), true);
for _ in 0..n {
loop {
match cursor.next_boundary(chunk, chunk_byte_index) {
Ok(None) => return slice.len_chars(),
Ok(Some(boundry_offset)) => {
byte_index = boundry_offset;
break;
}
Err(GraphemeIncomplete::NextChunk) => {
chunk_byte_index += chunk.len();
let (a, _, c, _) = slice.chunk_at_byte(chunk_byte_index);
chunk = a;
chunk_char_index = c;
}
Err(GraphemeIncomplete::PreContext(n)) => {
let ctx_chunk = slice.chunk_at_byte(n - 1).0;
cursor.provide_context(ctx_chunk, n - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
let tmp = str_utils::byte_to_char_idx(chunk, byte_index - chunk_byte_index);
chunk_char_index + tmp
}
#[cfg(test)]
mod tests {
use super::*;
use ropey::Rope;
#[test]
fn prev_grapheme_1() {
let text = Rope::from(MULTI_CHAR_EMOJI);
let grapheme_start = text.prev_grapheme_boundary(text.len_chars() - 1);
assert_eq!(0, grapheme_start);
}
#[test]
fn end_grapheme_1() {
let text = Rope::from(MULTI_CHAR_EMOJI);
let grapheme_end = text.next_grapheme_boundary(0);
assert_eq!(text.len_chars(), grapheme_end);
}
const MULTI_CHAR_EMOJI: &str = r#"๐จโ๐จโ๐งโ๐ง"#;
}