use std::borrow::Cow;
use std::ops::Range;
use unicode_segmentation::GraphemeCursor;
pub trait EditableText: Sized {
fn cursor(&self, position: usize) -> Option<StringCursor>;
fn edit(&mut self, range: Range<usize>, new: impl Into<String>);
fn slice(&self, range: Range<usize>) -> Option<Cow<str>>;
fn len(&self) -> usize;
fn prev_grapheme_offset(&self, offset: usize) -> Option<usize>;
fn next_grapheme_offset(&self, offset: usize) -> Option<usize>;
fn prev_codepoint_offset(&self, offset: usize) -> Option<usize>;
fn next_codepoint_offset(&self, offset: usize) -> Option<usize>;
fn is_empty(&self) -> bool;
fn from_str(s: &str) -> Self;
}
impl EditableText for String {
fn cursor<'a>(&self, position: usize) -> Option<StringCursor> {
let new_cursor = StringCursor {
text: &self,
position,
};
if new_cursor.is_boundary() {
Some(new_cursor)
} else {
None
}
}
fn edit(&mut self, range: Range<usize>, new: impl Into<String>) {
self.replace_range(range, &new.into());
}
fn slice(&self, range: Range<usize>) -> Option<Cow<str>> {
if let Some(slice) = self.get(range) {
Some(Cow::from(slice))
} else {
None
}
}
fn len(&self) -> usize {
self.len()
}
fn prev_grapheme_offset(&self, from: usize) -> Option<usize> {
let mut c = GraphemeCursor::new(from, self.len(), true);
c.prev_boundary(self, 0).unwrap()
}
fn next_grapheme_offset(&self, from: usize) -> Option<usize> {
let mut c = GraphemeCursor::new(from, self.len(), true);
c.next_boundary(self, 0).unwrap()
}
fn prev_codepoint_offset(&self, from: usize) -> Option<usize> {
let mut c = self.cursor(from).unwrap();
c.prev()
}
fn next_codepoint_offset(&self, from: usize) -> Option<usize> {
let mut c = self.cursor(from).unwrap();
if c.next().is_some() {
Some(c.pos())
} else {
None
}
}
fn is_empty(&self) -> bool {
self.is_empty()
}
fn from_str(s: &str) -> Self {
s.to_string()
}
}
pub trait EditableTextCursor<EditableText> {
fn set(&mut self, position: usize);
fn pos(&self) -> usize;
fn is_boundary(&self) -> bool;
fn prev(&mut self) -> Option<usize>;
fn next(&mut self) -> Option<usize>;
fn peek_next_codepoint(&self) -> Option<char>;
fn prev_codepoint(&mut self) -> Option<char>;
fn next_codepoint(&mut self) -> Option<char>;
fn at_or_next(&mut self) -> Option<usize>;
fn at_or_prev(&mut self) -> Option<usize>;
}
#[derive(Debug)]
pub struct StringCursor<'a> {
text: &'a str,
position: usize,
}
impl<'a> EditableTextCursor<&'a String> for StringCursor<'a> {
fn set(&mut self, position: usize) {
self.position = position;
}
fn pos(&self) -> usize {
self.position
}
fn is_boundary(&self) -> bool {
self.text.is_char_boundary(self.position)
}
fn prev(&mut self) -> Option<usize> {
let current_pos = self.pos();
if current_pos == 0 {
None
} else {
let mut len = 1;
while !self.text.is_char_boundary(current_pos - len) {
len += 1;
}
self.set(self.pos() - len);
Some(self.pos())
}
}
fn next(&mut self) -> Option<usize> {
let current_pos = self.pos();
if current_pos == self.text.len() {
None
} else {
let b = self.text.as_bytes()[current_pos];
self.set(current_pos + len_utf8_from_first_byte(b));
Some(current_pos)
}
}
fn peek_next_codepoint(&self) -> Option<char> {
self.text[self.pos()..].chars().next()
}
fn prev_codepoint(&mut self) -> Option<char> {
if let Some(prev) = self.prev() {
self.text[prev..].chars().next()
} else {
None
}
}
fn next_codepoint(&mut self) -> Option<char> {
let current_index = self.pos();
if self.next().is_some() {
self.text[current_index..].chars().next()
} else {
None
}
}
fn at_or_next(&mut self) -> Option<usize> {
if self.is_boundary() {
Some(self.pos())
} else {
self.next()
}
}
fn at_or_prev(&mut self) -> Option<usize> {
if self.is_boundary() {
Some(self.pos())
} else {
self.prev()
}
}
}
pub fn len_utf8_from_first_byte(b: u8) -> usize {
match b {
b if b < 0x80 => 1,
b if b < 0xe0 => 2,
b if b < 0xf0 => 3,
_ => 4,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replace() {
let mut a = String::from("hello world");
a.edit(1..9, "era");
assert_eq!("herald", String::from(a));
}
#[test]
fn prev_codepoint_offset() {
let a = String::from("a\u{00A1}\u{4E00}\u{1F4A9}");
assert_eq!(Some(6), a.prev_codepoint_offset(10));
assert_eq!(Some(3), a.prev_codepoint_offset(6));
assert_eq!(Some(1), a.prev_codepoint_offset(3));
assert_eq!(Some(0), a.prev_codepoint_offset(1));
assert_eq!(None, a.prev_codepoint_offset(0));
let b = a.slice(1..10).unwrap().to_string();
assert_eq!(Some(5), b.prev_codepoint_offset(9));
assert_eq!(Some(2), b.prev_codepoint_offset(5));
assert_eq!(Some(0), b.prev_codepoint_offset(2));
assert_eq!(None, b.prev_codepoint_offset(0));
}
#[test]
fn next_codepoint_offset() {
let a = String::from("a\u{00A1}\u{4E00}\u{1F4A9}");
assert_eq!(Some(10), a.next_codepoint_offset(6));
assert_eq!(Some(6), a.next_codepoint_offset(3));
assert_eq!(Some(3), a.next_codepoint_offset(1));
assert_eq!(Some(1), a.next_codepoint_offset(0));
assert_eq!(None, a.next_codepoint_offset(10));
let b = a.slice(1..10).unwrap().to_string();
assert_eq!(Some(9), b.next_codepoint_offset(5));
assert_eq!(Some(5), b.next_codepoint_offset(2));
assert_eq!(Some(2), b.next_codepoint_offset(0));
assert_eq!(None, b.next_codepoint_offset(9));
}
#[test]
fn prev_next() {
let input = String::from("abc");
let mut cursor = input.cursor(0).unwrap();
assert_eq!(cursor.next(), Some(0));
assert_eq!(cursor.next(), Some(1));
assert_eq!(cursor.prev(), Some(1));
assert_eq!(cursor.next(), Some(1));
assert_eq!(cursor.next(), Some(2));
}
#[test]
fn peek_next_codepoint() {
let inp = String::from("$¢€£💶");
let mut cursor = inp.cursor(0).unwrap();
assert_eq!(cursor.peek_next_codepoint(), Some('$'));
assert_eq!(cursor.peek_next_codepoint(), Some('$'));
assert_eq!(cursor.next_codepoint(), Some('$'));
assert_eq!(cursor.peek_next_codepoint(), Some('¢'));
assert_eq!(cursor.prev_codepoint(), Some('$'));
assert_eq!(cursor.peek_next_codepoint(), Some('$'));
assert_eq!(cursor.next_codepoint(), Some('$'));
assert_eq!(cursor.next_codepoint(), Some('¢'));
assert_eq!(cursor.peek_next_codepoint(), Some('€'));
assert_eq!(cursor.next_codepoint(), Some('€'));
assert_eq!(cursor.peek_next_codepoint(), Some('£'));
assert_eq!(cursor.next_codepoint(), Some('£'));
assert_eq!(cursor.peek_next_codepoint(), Some('💶'));
assert_eq!(cursor.next_codepoint(), Some('💶'));
assert_eq!(cursor.peek_next_codepoint(), None);
assert_eq!(cursor.next_codepoint(), None);
assert_eq!(cursor.peek_next_codepoint(), None);
}
#[test]
fn prev_grapheme_offset() {
let a = String::from("A\u{030a}\u{110b}\u{1161}\u{1f1fa}\u{1f1f8}");
assert_eq!(Some(9), a.prev_grapheme_offset(17));
assert_eq!(Some(3), a.prev_grapheme_offset(9));
assert_eq!(Some(0), a.prev_grapheme_offset(3));
assert_eq!(None, a.prev_grapheme_offset(0));
}
#[test]
fn next_grapheme_offset() {
let a = String::from("A\u{030a}\u{110b}\u{1161}\u{1f1fa}\u{1f1f8}");
assert_eq!(Some(3), a.next_grapheme_offset(0));
assert_eq!(Some(9), a.next_grapheme_offset(3));
assert_eq!(Some(17), a.next_grapheme_offset(9));
assert_eq!(None, a.next_grapheme_offset(17));
}
}