use unicode_segmentation::UnicodeSegmentation;
use crate::core_editor::graphemes::{ensure_grapheme_boundary_prev, next_grapheme_boundary};
use crate::core_editor::line_buffer::is_whitespace_str;
use crate::enums::{Direction, WordEdge, WordKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CharClass {
Word,
Punctuation,
Whitespace,
Eol,
}
pub(crate) fn categorize_char(ch: char) -> CharClass {
match ch {
'\n' | '\r' => CharClass::Eol,
ch if ch.is_alphanumeric() || ch == '_' => CharClass::Word,
ch if ch.is_whitespace() => CharClass::Whitespace,
_ => CharClass::Punctuation,
}
}
pub(crate) fn is_word_boundary(a: char, b: char) -> bool {
categorize_char(a) != categorize_char(b)
}
pub(crate) fn is_long_word_boundary(a: char, b: char) -> bool {
match (categorize_char(a), categorize_char(b)) {
(CharClass::Word, CharClass::Punctuation) | (CharClass::Punctuation, CharClass::Word) => {
false
}
(a, b) => a != b,
}
}
pub(crate) fn locate_word(
buf: &str,
origin: usize,
kind: WordKind,
edge: WordEdge,
direction: Direction,
) -> usize {
if kind == WordKind::Unicode {
return locate_unicode_word(buf, origin, edge, direction);
}
let forward = direction == Direction::Forward;
let is_boundary: fn(char, char) -> bool = match kind {
WordKind::Word => is_word_boundary,
WordKind::LongWord => is_long_word_boundary,
WordKind::Unicode => unreachable!("handled above"),
};
let is_target = |before: Option<char>, ch: char, after: Option<char>| -> bool {
if matches!(categorize_char(ch), CharClass::Whitespace | CharClass::Eol) {
return false;
}
match edge {
WordEdge::Start => before.map_or(true, |b| is_boundary(b, ch)),
WordEdge::End => after.map_or(true, |a| is_boundary(ch, a)),
}
};
if forward {
let mut before = buf[..origin].chars().next_back();
let mut iter = buf[origin..]
.char_indices()
.map(|(i, c)| (origin + i, c))
.peekable();
while let Some((byte, ch)) = iter.next() {
let after = iter.peek().map(|&(_, c)| c);
if is_target(before, ch, after) {
let boundary = match edge {
WordEdge::Start => ensure_grapheme_boundary_prev(buf, byte),
WordEdge::End => next_grapheme_boundary(buf, byte),
};
if boundary > origin {
return boundary;
}
}
before = Some(ch);
}
buf.len()
} else {
let mut after = buf[origin..].chars().next();
let mut iter = buf[..origin].char_indices().rev().peekable();
while let Some((byte, ch)) = iter.next() {
let before = iter.peek().map(|&(_, c)| c);
if is_target(before, ch, after) {
return ensure_grapheme_boundary_prev(buf, byte);
}
after = Some(ch);
}
0
}
}
fn locate_unicode_word(buf: &str, origin: usize, edge: WordEdge, direction: Direction) -> usize {
let forward = direction == Direction::Forward;
match (forward, edge) {
(true, WordEdge::Start) => buf[origin..]
.split_word_bound_indices()
.find(|(i, w)| *i != 0 && !is_whitespace_str(w))
.map_or(buf.len(), |(i, _)| origin + i),
(true, WordEdge::End) => buf[origin..]
.split_word_bound_indices()
.filter(|(_, w)| !is_whitespace_str(w))
.find_map(|(i, w)| (origin + i + w.len() > origin).then_some(origin + i + w.len()))
.unwrap_or(buf.len()),
(false, WordEdge::Start) => buf[..origin]
.split_word_bound_indices()
.rfind(|(_, w)| !is_whitespace_str(w))
.map_or(0, |(i, _)| i),
(false, WordEdge::End) => buf[..origin]
.split_word_bound_indices()
.rev()
.find(|(_, w)| !is_whitespace_str(w))
.and_then(|(i, w)| w.grapheme_indices(true).next_back().map(|(gi, _)| i + gi))
.unwrap_or(0),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn categorize_each_class() {
assert_eq!(categorize_char('a'), CharClass::Word);
assert_eq!(categorize_char('Z'), CharClass::Word);
assert_eq!(categorize_char('7'), CharClass::Word);
assert_eq!(categorize_char('_'), CharClass::Word);
assert_eq!(categorize_char('é'), CharClass::Word); assert_eq!(categorize_char(' '), CharClass::Whitespace);
assert_eq!(categorize_char('\t'), CharClass::Whitespace);
assert_eq!(categorize_char('\n'), CharClass::Eol);
assert_eq!(categorize_char('\r'), CharClass::Eol);
assert_eq!(categorize_char('.'), CharClass::Punctuation);
assert_eq!(categorize_char('-'), CharClass::Punctuation);
}
#[test]
fn carriage_return_is_eol_not_a_word() {
assert_eq!(
locate_word(
"ab\r\ncd",
0,
WordKind::Word,
WordEdge::End,
Direction::Forward
),
2
);
assert_eq!(
locate_word(
"ab\r\ncd",
2,
WordKind::Word,
WordEdge::End,
Direction::Forward
),
6
);
}
#[test]
fn small_word_boundary_is_any_class_change() {
assert!(is_word_boundary('a', '.')); assert!(is_word_boundary('.', 'a')); assert!(is_word_boundary('a', ' ')); assert!(is_word_boundary(' ', '\n')); assert!(!is_word_boundary('a', 'b')); assert!(!is_word_boundary('.', ',')); }
#[test]
fn big_word_boundary_fuses_word_and_punctuation() {
assert!(!is_long_word_boundary('o', '.'));
assert!(!is_long_word_boundary('.', 'b'));
assert!(is_long_word_boundary('a', ' '));
assert!(is_long_word_boundary('.', ' '));
assert!(is_long_word_boundary(' ', '\n'));
assert!(!is_long_word_boundary('a', 'b')); }
}