use super::clamp_to_boundary;
fn is_keyword_character(character: char) -> bool {
character == '_' || character.is_alphanumeric()
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum WordClass {
Keyword,
Punctuation,
}
impl WordClass {
fn normal(character: char) -> Option<Self> {
if character.is_whitespace() {
None
} else if is_keyword_character(character) {
Some(Self::Keyword)
} else {
Some(Self::Punctuation)
}
}
fn big(character: char) -> Option<Self> {
(!character.is_whitespace()).then_some(Self::Keyword)
}
}
pub(super) fn next_word_start(text: &str, index: usize) -> usize {
next_classified_word_start(text, index, WordClass::normal)
}
pub(super) fn next_big_word_start(text: &str, index: usize) -> usize {
next_classified_word_start(text, index, WordClass::big)
}
pub(super) fn previous_word_start(text: &str, index: usize) -> usize {
previous_classified_word_start(text, index, WordClass::normal)
}
pub(super) fn previous_big_word_start(text: &str, index: usize) -> usize {
previous_classified_word_start(text, index, WordClass::big)
}
pub(super) fn next_word_end(text: &str, index: usize) -> usize {
next_classified_word_end(text, index, WordClass::normal)
}
pub(super) fn next_big_word_end(text: &str, index: usize) -> usize {
next_classified_word_end(text, index, WordClass::big)
}
pub(super) fn previous_word_end(text: &str, index: usize) -> usize {
previous_classified_word_end(text, index, WordClass::normal)
}
pub(super) fn previous_big_word_end(text: &str, index: usize) -> usize {
previous_classified_word_end(text, index, WordClass::big)
}
fn next_classified_word_start(
text: &str,
index: usize,
classify: fn(char) -> Option<WordClass>,
) -> usize {
let index = clamp_to_boundary(text, index);
let current_class = text[index..].chars().next().and_then(classify);
let mut previous_class = current_class;
let mut left_current_word = current_class.is_none();
for (offset, character) in text[index..].char_indices() {
let byte_index = index + offset;
let class = classify(character);
if byte_index == index {
continue;
}
match (previous_class, class) {
(_, None) => left_current_word = true,
(Some(previous), Some(current)) if previous != current => return byte_index,
(None, Some(_)) if left_current_word => return byte_index,
_ => {}
}
previous_class = class;
}
text.len()
}
fn previous_classified_word_start(
text: &str,
index: usize,
classify: fn(char) -> Option<WordClass>,
) -> usize {
let index = clamp_to_boundary(text, index);
word_runs(text, classify)
.into_iter()
.take_while(|run| run.start < index)
.last()
.map_or(0, |run| run.start)
}
fn next_classified_word_end(
text: &str,
index: usize,
classify: fn(char) -> Option<WordClass>,
) -> usize {
let index = clamp_to_boundary(text, index);
for run in word_runs(text, classify) {
if index < run.start {
return run.end;
}
if index <= run.end {
return if index < run.end {
run.end
} else {
word_runs(text, classify)
.into_iter()
.find(|candidate| candidate.start > run.start)
.map_or(run.end, |candidate| candidate.end)
};
}
}
text.len()
}
fn previous_classified_word_end(
text: &str,
index: usize,
classify: fn(char) -> Option<WordClass>,
) -> usize {
let index = clamp_to_boundary(text, index);
let runs = word_runs(text, classify);
for (run_index, run) in runs.iter().enumerate().rev() {
if index > run.end {
return run.end;
}
if index > run.start {
return run_index
.checked_sub(1)
.and_then(|previous| runs.get(previous))
.map_or(0, |previous| previous.end);
}
}
0
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct WordRun {
start: usize,
end: usize,
class: WordClass,
}
fn word_runs(text: &str, classify: fn(char) -> Option<WordClass>) -> Vec<WordRun> {
let mut runs = Vec::new();
let mut current: Option<WordRun> = None;
for (byte_index, character) in text.char_indices() {
match (current, classify(character)) {
(Some(mut run), Some(class)) if run.class == class => {
run.end = byte_index;
current = Some(run);
}
(Some(run), Some(class)) => {
runs.push(run);
current = Some(WordRun {
start: byte_index,
end: byte_index,
class,
});
}
(None, Some(class)) => {
current = Some(WordRun {
start: byte_index,
end: byte_index,
class,
});
}
(Some(run), None) => {
runs.push(run);
current = None;
}
(None, None) => {}
}
}
if let Some(run) = current {
runs.push(run);
}
runs
}