use crate::editor::{CharClass, Cursor, classify};
pub(super) fn word_forward_char_class(lines: &[String], from: Cursor) -> Cursor {
let chars: Vec<char> = lines[from.row].chars().collect();
let mut i = from.col;
if i < chars.len() {
let start_class = classify(chars[i]);
if start_class != CharClass::Space {
while i < chars.len() && classify(chars[i]) == start_class {
i += 1;
}
}
while i < chars.len() && classify(chars[i]) == CharClass::Space {
i += 1;
}
if i < chars.len() {
return Cursor {
row: from.row,
col: i,
};
}
}
let mut row = from.row + 1;
while row < lines.len() {
let cs: Vec<char> = lines[row].chars().collect();
if cs.is_empty() {
return Cursor { row, col: 0 };
}
if let Some(col) = cs.iter().position(|&c| classify(c) != CharClass::Space) {
return Cursor { row, col };
}
row += 1;
}
Cursor {
row: from.row,
col: chars.len().saturating_sub(1),
}
}
pub(super) fn word_end_char_class(lines: &[String], from: Cursor, big: bool) -> Cursor {
let class_of = |c: char| {
let raw = classify(c);
if big && raw == CharClass::Punct {
CharClass::Word
} else {
raw
}
};
let mut row = from.row;
let mut col = from.col;
loop {
let chars: Vec<char> = lines[row].chars().collect();
let mut i = col.saturating_add(1);
while i < chars.len() && classify(chars[i]) == CharClass::Space {
i += 1;
}
if i < chars.len() {
let cls = class_of(chars[i]);
while i + 1 < chars.len() && class_of(chars[i + 1]) == cls {
i += 1;
}
return Cursor { row, col: i };
}
if row + 1 >= lines.len() {
return Cursor {
row,
col: chars.len().saturating_sub(1),
};
}
row += 1;
col = 0_usize.wrapping_sub(1); }
}
pub(super) fn word_end_back(lines: &[String], from: Cursor, big: bool) -> Cursor {
let class_of = |c: char| {
let raw = classify(c);
if big && raw == CharClass::Punct {
CharClass::Word
} else {
raw
}
};
let is_end = |chars: &[char], i: usize| {
if classify(chars[i]) == CharClass::Space {
return false;
}
if i + 1 >= chars.len() {
return true;
}
class_of(chars[i]) != class_of(chars[i + 1])
};
let mut row = from.row;
let mut start: Option<usize> = if from.col == 0 {
None
} else {
Some(from.col - 1)
};
loop {
let chars: Vec<char> = lines[row].chars().collect();
if let Some(mut i) = start {
loop {
if i < chars.len() && is_end(&chars, i) {
return Cursor { row, col: i };
}
if i == 0 {
break;
}
i -= 1;
}
}
if row == 0 {
return Cursor { row: 0, col: 0 };
}
row -= 1;
let len = lines[row].chars().count();
start = if len > 0 { Some(len - 1) } else { None };
}
}
pub(super) fn big_word_forward(lines: &[String], from: Cursor) -> Cursor {
let chars: Vec<char> = lines[from.row].chars().collect();
let mut i = from.col;
if i < chars.len() {
while i < chars.len() && classify(chars[i]) != CharClass::Space {
i += 1;
}
while i < chars.len() && classify(chars[i]) == CharClass::Space {
i += 1;
}
if i < chars.len() {
return Cursor {
row: from.row,
col: i,
};
}
}
let mut row = from.row + 1;
while row < lines.len() {
let cs: Vec<char> = lines[row].chars().collect();
if cs.is_empty() {
return Cursor { row, col: 0 };
}
if let Some(col) = cs.iter().position(|&c| classify(c) != CharClass::Space) {
return Cursor { row, col };
}
row += 1;
}
Cursor {
row: from.row,
col: chars.len().saturating_sub(1),
}
}
pub(super) fn big_word_back(lines: &[String], from: Cursor) -> Cursor {
if from.col == 0 {
if from.row > 0 {
let row = from.row - 1;
let col = lines[row].chars().count().saturating_sub(1);
return Cursor { row, col };
}
return from;
}
let chars: Vec<char> = lines[from.row].chars().collect();
let mut i = from.col.saturating_sub(1);
while i > 0 && classify(chars[i]) == CharClass::Space {
i -= 1;
}
while i > 0 && classify(chars[i - 1]) != CharClass::Space {
i -= 1;
}
Cursor {
row: from.row,
col: i,
}
}
pub(super) fn word_back_char_class(lines: &[String], from: Cursor) -> Cursor {
if from.col == 0 {
if from.row > 0 {
let row = from.row - 1;
let col = lines[row].chars().count().saturating_sub(1);
return Cursor { row, col };
}
return from;
}
let chars: Vec<char> = lines[from.row].chars().collect();
let mut i = from.col.saturating_sub(1);
while i > 0 && classify(chars[i]) == CharClass::Space {
i -= 1;
}
let target_class = classify(chars[i]);
while i > 0 && classify(chars[i - 1]) == target_class {
i -= 1;
}
Cursor {
row: from.row,
col: i,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lines(s: &str) -> Vec<String> {
s.split('\n').map(|s| s.to_string()).collect()
}
fn fwd(buf: &[String], row: usize, col: usize) -> (usize, usize) {
let c = word_forward_char_class(buf, Cursor { row, col });
(c.row, c.col)
}
fn back(buf: &[String], row: usize, col: usize) -> (usize, usize) {
let c = word_back_char_class(buf, Cursor { row, col });
(c.row, c.col)
}
#[test]
fn word_walks_into_punctuation_not_through_it() {
let l = lines("foo(bar)");
assert_eq!(fwd(&l, 0, 0), (0, 3)); assert_eq!(fwd(&l, 0, 3), (0, 4)); assert_eq!(fwd(&l, 0, 4), (0, 7)); }
#[test]
fn word_skips_whitespace() {
let l = lines("a b");
assert_eq!(fwd(&l, 0, 0), (0, 4));
}
#[test]
fn back_groups_punctuation_runs() {
let l = lines("a => b");
assert_eq!(back(&l, 0, 5), (0, 2)); assert_eq!(back(&l, 0, 2), (0, 0)); }
fn end(buf: &[String], row: usize, col: usize, big: bool) -> (usize, usize) {
let c = word_end_char_class(buf, Cursor { row, col }, big);
(c.row, c.col)
}
fn big_fwd(buf: &[String], row: usize, col: usize) -> (usize, usize) {
let c = big_word_forward(buf, Cursor { row, col });
(c.row, c.col)
}
fn big_back(buf: &[String], row: usize, col: usize) -> (usize, usize) {
let c = big_word_back(buf, Cursor { row, col });
(c.row, c.col)
}
#[test]
fn word_end_lands_on_last_char_of_word() {
let l = lines("foo bar");
assert_eq!(end(&l, 0, 0, false), (0, 2));
assert_eq!(end(&l, 0, 2, false), (0, 6));
}
#[test]
fn word_end_walks_into_punctuation_not_through_it() {
let l = lines("foo(bar)");
assert_eq!(end(&l, 0, 0, false), (0, 2)); assert_eq!(end(&l, 0, 2, false), (0, 3)); assert_eq!(end(&l, 0, 3, false), (0, 6)); assert_eq!(end(&l, 0, 6, false), (0, 7)); }
#[test]
fn big_word_end_collapses_punctuation() {
let l = lines("foo(bar)");
assert_eq!(end(&l, 0, 0, true), (0, 7));
}
#[test]
fn word_forward_stops_on_empty_lines() {
let l = lines("foo\n\nbar");
assert_eq!(fwd(&l, 0, 2), (1, 0));
assert_eq!(fwd(&l, 1, 0), (2, 0));
}
#[test]
fn word_forward_skips_blank_lines_to_first_word() {
let l = lines("foo\n \nbar");
assert_eq!(fwd(&l, 0, 2), (2, 0));
}
#[test]
fn big_word_forward_stops_on_empty_lines() {
let l = lines("foo\n\nbar");
assert_eq!(big_fwd(&l, 0, 2), (1, 0));
assert_eq!(big_fwd(&l, 1, 0), (2, 0));
}
#[test]
fn big_word_forward_skips_punctuation() {
let l = lines("foo(bar) baz");
assert_eq!(big_fwd(&l, 0, 0), (0, 9));
}
#[test]
fn big_word_back_skips_punctuation() {
let l = lines("foo(bar) baz");
assert_eq!(big_back(&l, 0, 9), (0, 0));
}
fn end_back(buf: &[String], row: usize, col: usize, big: bool) -> (usize, usize) {
let c = word_end_back(buf, Cursor { row, col }, big);
(c.row, c.col)
}
#[test]
fn word_end_back_lands_on_previous_word_end() {
let l = lines("foo bar baz");
assert_eq!(end_back(&l, 0, 8, false), (0, 6));
assert_eq!(end_back(&l, 0, 6, false), (0, 2));
}
#[test]
fn word_end_back_treats_punctuation_as_its_own_word() {
let l = lines("foo(bar)");
assert_eq!(end_back(&l, 0, 7, false), (0, 6));
assert_eq!(end_back(&l, 0, 7, true), (0, 0));
}
}