@@ -3,9 +3,13 @@ use std::iter::{self, from_fn, Peekable, SkipWhile};
3use ropey::iter::Chars;
3use ropey::iter::Chars;
5use crate::{
5use crate::{
7+ categorize_char, char_is_line_ending, char_is_punctuation, char_is_whitespace,
8+ char_is_word, CharCategory,
6 coords_at_pos, get_line_ending,
10 coords_at_pos, get_line_ending,
7 graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary},
11 graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary},
8− line_end, pos_at_coords, Position, Range, RopeSlice,
12+ line_end_char_index, pos_at_coords, Position, Range, RopeSlice,
11#[derive(Debug, Copy, Clone, PartialEq, Eq)]
15#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -37,9 +41,8 @@ pub fn move_horizontally(
37 nth_prev_grapheme_boundary(slice, pos, count).max(start)
41 nth_prev_grapheme_boundary(slice, pos, count).max(start)
39 Direction::Forward => {
43 Direction::Forward => {
40− // Line end is pos at the start of next line - 1
44+ let end_char_idx = line_end_char_index(&slice, line);
41− let end = line_end(&slice, line);
42− nth_next_grapheme_boundary(slice, pos, count).min(end)
45+ nth_next_grapheme_boundary(slice, pos, count).min(end_char_idx)
45 let anchor = match behaviour {
48 let anchor = match behaviour {
@@ -68,8 +71,11 @@ pub fn move_vertically(
71− // convert to 0-indexed, subtract another 1 because len_chars() counts \n
74+ // Length of the line sans line-ending.
72− let new_line_len = slice.line(new_line).len_chars().saturating_sub(2);
75+ let new_line_len = {
76+ let line = slice.line(new_line);
77+ line.len_chars() - get_line_ending(&line).map(|le| le.len_chars()).unwrap_or(0)
74 let new_col = std::cmp::min(horiz as usize, new_line_len);
80 let new_col = std::cmp::min(horiz as usize, new_line_len);
@@ -104,64 +110,6 @@ fn word_move(slice: RopeSlice, mut range: Range, count: usize, target: WordMotio
106// ---- util ------------
112// ---- util ------------
108−pub(crate) fn is_word(ch: char) -> bool {
109− ch.is_alphanumeric() || ch == '_'
113−pub(crate) fn is_end_of_line(ch: char) -> bool {
118−// Whitespace, but not end of line
119−pub(crate) fn is_strict_whitespace(ch: char) -> bool {
120− ch.is_whitespace() && !is_end_of_line(ch)
124−pub(crate) fn is_punctuation(ch: char) -> bool {
125− use unicode_general_category::{get_general_category, GeneralCategory};
128− get_general_category(ch),
129− GeneralCategory::OtherPunctuation
130− | GeneralCategory::OpenPunctuation
131− | GeneralCategory::ClosePunctuation
132− | GeneralCategory::InitialPunctuation
133− | GeneralCategory::FinalPunctuation
134− | GeneralCategory::ConnectorPunctuation
135− | GeneralCategory::DashPunctuation
136− | GeneralCategory::MathSymbol
137− | GeneralCategory::CurrencySymbol
138− | GeneralCategory::ModifierSymbol
142−#[derive(Debug, Eq, PartialEq)]
152−pub(crate) fn categorize(ch: char) -> Category {
153− if is_end_of_line(ch) {
155− } else if ch.is_whitespace() {
156− Category::Whitespace
157− } else if is_word(ch) {
159− } else if is_punctuation(ch) {
160− Category::Punctuation
167/// Returns first index that doesn't satisfy a given predicate when
115/// Returns first index that doesn't satisfy a given predicate when
@@ -235,7 +183,8 @@ impl CharHelpers for Chars<'_> {
235 let mut phase = WordMotionPhase::Start;
183 let mut phase = WordMotionPhase::Start;
236 let mut head = origin.head;
184 let mut head = origin.head;
237 let mut anchor: Option<usize> = None;
185 let mut anchor: Option<usize> = None;
238− let is_boundary = |a: char, b: Option<char>| categorize(a) != categorize(b.unwrap_or(a));
187+ |a: char, b: Option<char>| categorize_char(a) != categorize_char(b.unwrap_or(a));
239 while let Some(peek) = characters.peek().copied() {
188 while let Some(peek) = characters.peek().copied() {
240 phase = match phase {
189 phase = match phase {
241 WordMotionPhase::Start => {
190 WordMotionPhase::Start => {
@@ -244,7 +193,8 @@ impl CharHelpers for Chars<'_> {
244 break; // We're at the end, so there's nothing to do.
193 break; // We're at the end, so there's nothing to do.
246 // Anchor may remain here if the head wasn't at a boundary
195 // Anchor may remain here if the head wasn't at a boundary
247− if !is_boundary(peek, characters.peek().copied()) && !is_end_of_line(peek) {
196+ if !is_boundary(peek, characters.peek().copied()) && !char_is_line_ending(peek)
248 anchor = Some(head);
198 anchor = Some(head);
250 // First character is always skipped by the head
200 // First character is always skipped by the head
@@ -252,7 +202,7 @@ impl CharHelpers for Chars<'_> {
252 WordMotionPhase::SkipNewlines
202 WordMotionPhase::SkipNewlines
254 WordMotionPhase::SkipNewlines => {
204 WordMotionPhase::SkipNewlines => {
255− if is_end_of_line(peek) {
205+ if char_is_line_ending(peek) {
256 characters.next();
206 characters.next();
257 if characters.peek().is_some() {
207 if characters.peek().is_some() {
258 advance(&mut head);
208 advance(&mut head);
@@ -286,12 +236,12 @@ fn reached_target(target: WordMotionTarget, peek: char, next_peek: Option<&char>
287 match target {
237 match target {
288 WordMotionTarget::NextWordStart => {
238 WordMotionTarget::NextWordStart => {
289− ((categorize(peek) != categorize(*next_peek))
239+ ((categorize_char(peek) != categorize_char(*next_peek))
290− && (is_end_of_line(*next_peek) || !next_peek.is_whitespace()))
240+ && (char_is_line_ending(*next_peek) || !next_peek.is_whitespace()))
292 WordMotionTarget::NextWordEnd | WordMotionTarget::PrevWordStart => {
242 WordMotionTarget::NextWordEnd | WordMotionTarget::PrevWordStart => {
293− ((categorize(peek) != categorize(*next_peek))
243+ ((categorize_char(peek) != categorize_char(*next_peek))
294− && (!peek.is_whitespace() || is_end_of_line(*next_peek)))
244+ && (!peek.is_whitespace() || char_is_line_ending(*next_peek)))
@@ -330,7 +280,7 @@ mod test {
331 move_vertically(slice, range, Direction::Forward, 1, Movement::Move).head
281 move_vertically(slice, range, Direction::Forward, 1, Movement::Move).head
333− (1, 2).into()
283+ (1, 3).into()
@@ -343,12 +293,12 @@ mod test {
343 let mut range = Range::point(position);
293 let mut range = Range::point(position);
345 let moves_and_expected_coordinates = [
295 let moves_and_expected_coordinates = [
346− ((Direction::Forward, 1usize), (0, 1)),
296+ ((Direction::Forward, 1usize), (0, 1)), // T|his is a simple alphabetic line
347− ((Direction::Forward, 2usize), (0, 3)),
297+ ((Direction::Forward, 2usize), (0, 3)), // Thi|s is a simple alphabetic line
348− ((Direction::Forward, 0usize), (0, 3)),
298+ ((Direction::Forward, 0usize), (0, 3)), // Thi|s is a simple alphabetic line
349− ((Direction::Forward, 999usize), (0, 31)),
299+ ((Direction::Forward, 999usize), (0, 32)), // This is a simple alphabetic line|
350− ((Direction::Forward, 999usize), (0, 31)),
300+ ((Direction::Forward, 999usize), (0, 32)), // This is a simple alphabetic line|
351− ((Direction::Backward, 999usize), (0, 0)),
301+ ((Direction::Backward, 999usize), (0, 0)), // |This is a simple alphabetic line
354 for ((direction, amount), coordinates) in IntoIter::new(moves_and_expected_coordinates) {
304 for ((direction, amount), coordinates) in IntoIter::new(moves_and_expected_coordinates) {
@@ -366,15 +316,15 @@ mod test {
366 let mut range = Range::point(position);
316 let mut range = Range::point(position);
368 let moves_and_expected_coordinates = IntoIter::new([
318 let moves_and_expected_coordinates = IntoIter::new([
369− ((Direction::Forward, 1usize), (0, 1)), // M_ltiline
319+ ((Direction::Forward, 1usize), (0, 1)), // M|ultiline\n
370− ((Direction::Forward, 2usize), (0, 3)), // Mul_iline
320+ ((Direction::Forward, 2usize), (0, 3)), // Mul|tiline\n
371− ((Direction::Backward, 6usize), (0, 0)), // _ultiline
321+ ((Direction::Backward, 6usize), (0, 0)), // |Multiline\n
372− ((Direction::Backward, 999usize), (0, 0)), // _ultiline
322+ ((Direction::Backward, 999usize), (0, 0)), // |Multiline\n
373− ((Direction::Forward, 3usize), (0, 3)), // Mul_iline
323+ ((Direction::Forward, 3usize), (0, 3)), // Mul|tiline\n
374− ((Direction::Forward, 0usize), (0, 3)), // Mul_iline
324+ ((Direction::Forward, 0usize), (0, 3)), // Mul|tiline\n
375− ((Direction::Backward, 0usize), (0, 3)), // Mul_iline
325+ ((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\n
376− ((Direction::Forward, 999usize), (0, 9)), // Multilin_
326+ ((Direction::Forward, 999usize), (0, 9)), // Multiline|\n
377− ((Direction::Forward, 999usize), (0, 9)), // Multilin_
327+ ((Direction::Forward, 999usize), (0, 9)), // Multiline|\n
380 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
330 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
@@ -446,7 +396,7 @@ mod test {
446 // First descent preserves column as the target line is wider
396 // First descent preserves column as the target line is wider
447 ((Axis::V, Direction::Forward, 1usize), (1, 8)),
397 ((Axis::V, Direction::Forward, 1usize), (1, 8)),
448 // Second descent clamps column as the target line is shorter
398 // Second descent clamps column as the target line is shorter
449− ((Axis::V, Direction::Forward, 1usize), (2, 4)),
399+ ((Axis::V, Direction::Forward, 1usize), (2, 5)),
450 // Third descent restores the original column
400 // Third descent restores the original column
451 ((Axis::V, Direction::Forward, 1usize), (3, 8)),
401 ((Axis::V, Direction::Forward, 1usize), (3, 8)),
452 // Behaviour is preserved even through long jumps
402 // Behaviour is preserved even through long jumps
@@ -760,45 +710,4 @@ mod test {
765− fn test_categorize() {
766− const WORD_TEST_CASE: &'static str =
767− "_hello_world_あいうえおー12345678901234567890";
768− const PUNCTUATION_TEST_CASE: &'static str =
769− "!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~!”#$%&’()*+、。:;<=>?@「」^`{|}~";
770− const WHITESPACE_TEST_CASE: &'static str = " ";
772− assert_eq!(Category::Eol, categorize('\n'));
774− for ch in WHITESPACE_TEST_CASE.chars() {
776− Category::Whitespace,
778− "Testing '{}', but got `{:?}` instead of `Category::Whitespace`",
784− for ch in WORD_TEST_CASE.chars() {
788− "Testing '{}', but got `{:?}` instead of `Category::Word`",
794− for ch in PUNCTUATION_TEST_CASE.chars() {
796− Category::Punctuation,
798− "Testing '{}', but got `{:?}` instead of `Category::Punctuation`",