use unicode_lang::{char_width, is_xid_continue};
#[inline]
fn width(c: char) -> usize {
if c.is_ascii() {
usize::from(!c.is_ascii_control())
} else {
char_width(c)
}
}
pub(crate) fn text_width(text: &str) -> usize {
if text.is_ascii() {
text.bytes().filter(|b| !b.is_ascii_control()).count()
} else {
text.chars().map(width).sum()
}
}
#[inline]
fn starts_unit(c: char) -> bool {
width(c) != 0
}
#[inline]
fn is_word(c: char) -> bool {
if c.is_ascii() {
c.is_ascii_alphanumeric() || c == '_'
} else {
is_xid_continue(c)
}
}
pub(crate) fn next_unit(line: &str, pos: usize) -> usize {
let mut chars = line[pos..].char_indices();
if chars.next().is_none() {
return pos;
}
chars
.find(|&(_, c)| starts_unit(c))
.map_or(line.len(), |(offset, _)| pos + offset)
}
pub(crate) fn prev_unit(line: &str, pos: usize) -> usize {
line[..pos]
.char_indices()
.rev()
.find(|&(_, c)| starts_unit(c))
.map_or(0, |(offset, _)| offset)
}
#[inline]
fn unit_at(line: &str, pos: usize) -> Option<char> {
line[pos..].chars().next()
}
pub(crate) fn word_right(line: &str, mut pos: usize) -> usize {
while unit_at(line, pos).is_some_and(|c| !is_word(c)) {
pos = next_unit(line, pos);
}
while unit_at(line, pos).is_some_and(is_word) {
pos = next_unit(line, pos);
}
pos
}
pub(crate) fn word_left(line: &str, mut pos: usize) -> usize {
while pos > 0 {
let start = prev_unit(line, pos);
if unit_at(line, start).is_some_and(is_word) {
break;
}
pos = start;
}
while pos > 0 {
let start = prev_unit(line, pos);
if !unit_at(line, start).is_some_and(is_word) {
break;
}
pos = start;
}
pos
}
#[cfg(test)]
mod tests {
use alloc::format;
use unicode_lang::str_width;
use super::*;
const ACUTE: char = '\u{0301}';
#[test]
fn test_next_unit_ascii_steps_one_byte() {
assert_eq!(next_unit("abc", 0), 1);
assert_eq!(next_unit("abc", 2), 3);
assert_eq!(next_unit("abc", 3), 3);
}
#[test]
fn test_next_unit_combining_mark_joins_base() {
let line = format!("e{ACUTE}x");
assert_eq!(next_unit(&line, 0), 3); assert_eq!(next_unit(&line, 3), 4);
}
#[test]
fn test_next_unit_multibyte_wide_char() {
assert_eq!(next_unit("世界", 0), 3);
assert_eq!(next_unit("世界", 3), 6);
}
#[test]
fn test_prev_unit_combining_mark_joins_base() {
let line = format!("ae{ACUTE}");
assert_eq!(prev_unit(&line, line.len()), 1);
assert_eq!(prev_unit(&line, 1), 0);
assert_eq!(prev_unit(&line, 0), 0);
}
#[test]
fn test_prev_unit_orphan_mark_at_start_goes_to_zero() {
let line = format!("{ACUTE}{ACUTE}");
assert_eq!(prev_unit(&line, line.len()), 0);
}
#[test]
fn test_word_right_skips_separators_then_word() {
let line = "let value = 1";
assert_eq!(word_right(line, 0), 3);
assert_eq!(word_right(line, 3), 10);
assert_eq!(word_right(line, 10), 14);
assert_eq!(word_right(line, 14), 14);
}
#[test]
fn test_word_left_skips_separators_then_word() {
let line = "let value = 1";
assert_eq!(word_left(line, 14), 13);
assert_eq!(word_left(line, 13), 5);
assert_eq!(word_left(line, 5), 0);
assert_eq!(word_left(line, 0), 0);
}
#[test]
fn test_word_motion_treats_underscore_and_digits_as_word() {
let line = "a_b9 + c";
assert_eq!(word_right(line, 0), 4);
assert_eq!(word_left(line, 4), 0);
}
#[test]
fn test_word_motion_keeps_accented_word_whole() {
let line = format!("caf{ACUTE}e! x");
let end = word_right(&line, 0);
assert_eq!(&line[..end], format!("caf{ACUTE}e"));
assert_eq!(word_left(&line, end), 0);
}
#[test]
fn test_ascii_fast_paths_agree_with_unicode_tables() {
for c in (0_u8..=0x7f).map(char::from) {
assert_eq!(width(c), char_width(c), "width of {c:?}");
assert_eq!(is_word(c), is_xid_continue(c), "word class of {c:?}");
}
let all: alloc::string::String = (0_u8..=0x7f).map(char::from).collect();
assert_eq!(text_width(&all), str_width(&all));
assert_eq!(text_width("a世e\u{0301}"), str_width("a世e\u{0301}"));
}
#[test]
fn test_word_motion_empty_line_stays_put() {
assert_eq!(word_right("", 0), 0);
assert_eq!(word_left("", 0), 0);
}
}