Skip to main content

kimun_notes/ropetext/
motion.rs

1//! Where a movement lands.
2//!
3//! Every motion is a function of a text and a position that *returns* a position.
4//! It never moves a cursor. That is what lets an operator ask where a movement
5//! would end without going there first — deleting to the end of a word is a
6//! delete over `cursor..word_end(text, cursor)`, not a move followed by a
7//! measurement of where the cursor got to.
8//!
9//! A position sits *between* clusters, so the far end of a row is a place. An
10//! editor whose cursor sits *on* a character has one fewer column per row and
11//! adapts at its own edge; this module does not take a view on that.
12//!
13//! # Failure
14//!
15//! A motion returns [`Position`] when it is defined as going as far as it can —
16//! moving right at the end of the text stays at the end. It returns
17//! `Option<Position>` when finding nothing has to be distinguishable from
18//! arriving somewhere, because an operator waiting on it must abort rather than
19//! act on a range that means something else. `f` with no matching character on the
20//! row is the plain case: deleting to it must delete nothing at all.
21
22use crate::ropetext::layout::{Cell, Layout, RowHints};
23use crate::ropetext::position::{Column, Position};
24use crate::ropetext::text::Text;
25
26/// What kind of character a cluster starts with, for word motions.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Class {
29    /// Space, tab, or the line break.
30    Blank,
31    /// Letters, digits, underscore — a word to a word motion.
32    Word,
33    /// Everything else that is not blank.
34    Punctuation,
35}
36
37/// How a word motion divides the text.
38///
39/// One motion implementation serves both by taking this: what distinguishes `w`
40/// from `W` is entirely which runs count as one word, so writing them separately
41/// means writing the same crossing-rows, empty-line and end-of-text logic twice
42/// and getting it subtly different in each.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Words {
45    /// Runs of word characters and runs of punctuation are separate words.
46    Small,
47    /// Any run of non-blanks is one word.
48    Big,
49}
50
51impl Words {
52    /// How this division classifies `c`.
53    ///
54    /// Public because a caller building its own ranges — vim's text objects, say —
55    /// must divide the text exactly as the motions do, and reimplementing the rule
56    /// is how the two drift apart.
57    pub fn class_of(self, c: char) -> Class {
58        self.classify(c)
59    }
60
61    fn classify(self, c: char) -> Class {
62        if c.is_whitespace() {
63            Class::Blank
64        } else if self == Words::Big || c.is_alphanumeric() || c == '_' {
65            Class::Word
66        } else {
67            Class::Punctuation
68        }
69    }
70}
71
72/// Where a vertical motion aims to land.
73///
74/// Vim calls the remembered column `curswant`, and it is why walking down through
75/// a short line and out the other side returns to the column you started in
76/// rather than the short line's end. Passed in rather than remembered here: this
77/// module holds no state, and the caller already knows whether the last motion was
78/// vertical.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum Goal {
81    /// Land in this column, or at the end of the row if it is shorter.
82    Column(Column),
83    /// Land at the end of the row, whatever its length.
84    RowEnd,
85}
86
87// -- within a row ------------------------------------------------------------
88
89/// One cluster right, stopping at the end of the row.
90pub fn right(text: &Text, from: Position) -> Position {
91    let end = row_end(text, from);
92    if from.byte() >= end.byte() {
93        return from;
94    }
95    position(text, text.next_cluster_byte(from.byte()))
96}
97
98/// One cluster left, stopping at the start of the row.
99pub fn left(text: &Text, from: Position) -> Position {
100    let start = row_start(text, from);
101    if from.byte() <= start.byte() {
102        return from;
103    }
104    position(text, text.prev_cluster_byte(from.byte()))
105}
106
107/// One cluster forward, crossing into the next row at the end of this one.
108pub fn next_cluster(text: &Text, from: Position) -> Position {
109    position(text, text.next_cluster_byte(from.byte()))
110}
111
112/// One cluster back, crossing into the previous row at the start of this one.
113pub fn prev_cluster(text: &Text, from: Position) -> Position {
114    position(text, text.prev_cluster_byte(from.byte()))
115}
116
117/// Column zero of this row.
118pub fn row_start(text: &Text, from: Position) -> Position {
119    text.position(from.row(), Column::ZERO)
120        .unwrap_or_else(|| text.start())
121}
122
123/// Just past the last cluster of this row, before its line break.
124pub fn row_end(text: &Text, from: Position) -> Position {
125    let len = text.line_len_chars(from.row()).unwrap_or(0);
126    text.position(from.row(), Column::new(len)).unwrap_or(from)
127}
128
129/// The first non-blank of this row, or its end if the row is all blanks.
130pub fn first_non_blank(text: &Text, from: Position) -> Position {
131    let Some(line) = text.line(from.row()) else {
132        return from;
133    };
134    let blanks = line.chars().take_while(|c| c.is_whitespace()).count();
135    text.position(from.row(), Column::new(blanks))
136        .unwrap_or_else(|| row_end(text, from))
137}
138
139/// Just past the last non-blank of this row, or its start if all blanks.
140pub fn last_non_blank(text: &Text, from: Position) -> Position {
141    let Some(line) = text.line(from.row()) else {
142        return from;
143    };
144    let trimmed = line.trim_end();
145    let chars = trimmed.chars().count();
146    text.position(from.row(), Column::new(chars))
147        .unwrap_or_else(|| row_end(text, from))
148}
149
150// -- across rows -------------------------------------------------------------
151
152/// The start of the text.
153pub fn text_start(text: &Text) -> Position {
154    text.start()
155}
156
157/// The end of the text.
158pub fn text_end(text: &Text) -> Position {
159    text.end()
160}
161
162/// The first non-blank of row `row`, clamped to the last row.
163pub fn goto_row(text: &Text, row: usize) -> Position {
164    let row = row.min(text.line_count().saturating_sub(1));
165    let at = text
166        .position(row, Column::ZERO)
167        .unwrap_or_else(|| text.start());
168    first_non_blank(text, at)
169}
170
171/// `rows` rows down (or up, when negative), aiming at `goal`.
172///
173/// Saturates at the first and last row rather than failing: holding a cursor key
174/// down at the end of a buffer should rest there.
175pub fn vertical(text: &Text, from: Position, rows: isize, goal: Goal) -> Position {
176    let last = text.line_count().saturating_sub(1);
177    let row = if rows >= 0 {
178        from.row().saturating_add(rows.unsigned_abs()).min(last)
179    } else {
180        from.row().saturating_sub(rows.unsigned_abs())
181    };
182    let len = text.line_len_chars(row).unwrap_or(0);
183    let column = match goal {
184        Goal::RowEnd => len,
185        Goal::Column(column) => column.get().min(len),
186    };
187    // A column landing inside a cluster is not addressable, so walk back to the
188    // cluster that contains it — the row below may join characters the row above
189    // kept apart.
190    let mut column = column;
191    loop {
192        if let Some(at) = text.position(row, Column::new(column)) {
193            return at;
194        }
195        if column == 0 {
196            return text.position(row, Column::ZERO).unwrap_or(from);
197        }
198        column -= 1;
199    }
200}
201
202/// The next paragraph break at or after `from`: a blank row, or the end of the
203/// text.
204///
205/// Saturates. Vim's `}`.
206pub fn paragraph_forward(text: &Text, from: Position) -> Position {
207    let last = text.line_count().saturating_sub(1);
208    let mut row = from.row() + 1;
209    while row <= last {
210        if is_blank_row(text, row) {
211            return text
212                .position(row, Column::ZERO)
213                .unwrap_or_else(|| text.end());
214        }
215        row += 1;
216    }
217    text.end()
218}
219
220/// The previous paragraph break before `from`. Vim's `{`.
221pub fn paragraph_back(text: &Text, from: Position) -> Position {
222    let mut row = from.row();
223    while row > 0 {
224        row -= 1;
225        if is_blank_row(text, row) {
226            return text
227                .position(row, Column::ZERO)
228                .unwrap_or_else(|| text.start());
229        }
230    }
231    text.start()
232}
233
234// -- words -------------------------------------------------------------------
235
236/// The start of the next word. Vim's `w` and `W`.
237///
238/// Saturates at the end of the text. An empty row is a word of its own, as it is
239/// in vim, so walking forward through a blank line stops on it.
240pub fn word_start_forward(text: &Text, from: Position, words: Words) -> Position {
241    let len = text.len_bytes();
242    let mut at = from.byte();
243    if at >= len {
244        return text.end();
245    }
246    let start_class = class_at(text, at, words);
247    let start_row = from.row();
248
249    // Leave the run the cursor is in.
250    if start_class != Class::Blank {
251        while at < len && class_at(text, at, words) == start_class {
252            at = text.next_cluster_byte(at);
253        }
254    }
255    // Then skip blanks — but an empty row is a stop in its own right.
256    while at < len && class_at(text, at, words) == Class::Blank {
257        // Not the row we started on, or a cursor already sitting on an empty row
258        // would be told to stay there and the motion would never move.
259        if is_empty_row_at(text, at) && text.row_of_byte(at) != start_row {
260            return position(text, at);
261        }
262        at = text.next_cluster_byte(at);
263    }
264    position(text, at)
265}
266
267/// The start of the current or previous word. Vim's `b` and `B`.
268pub fn word_start_back(text: &Text, from: Position, words: Words) -> Position {
269    let mut at = from.byte();
270    if at == 0 {
271        return text.start();
272    }
273    // Step off the cursor, then back over blanks. An empty row is a stop.
274    at = text.prev_cluster_byte(at);
275    while at > 0 && class_at(text, at, words) == Class::Blank {
276        if is_empty_row_at(text, at) {
277            return position(text, at);
278        }
279        at = text.prev_cluster_byte(at);
280    }
281    let run = class_at(text, at, words);
282    if run == Class::Blank {
283        return position(text, at);
284    }
285    // Walk to the front of the run.
286    while at > 0 {
287        let previous = text.prev_cluster_byte(at);
288        if class_at(text, previous, words) != run {
289            break;
290        }
291        at = previous;
292    }
293    position(text, at)
294}
295
296/// Just past the end of the current or next word. Vim's `e` and `E`.
297///
298/// `None` when there is no word ahead, so `de` at the end of a buffer deletes
299/// nothing rather than deleting to the end.
300pub fn word_end_forward(text: &Text, from: Position, words: Words) -> Option<Position> {
301    let len = text.len_bytes();
302    let mut at = text.next_cluster_byte(from.byte());
303    while at < len && class_at(text, at, words) == Class::Blank {
304        at = text.next_cluster_byte(at);
305    }
306    if at >= len {
307        return None;
308    }
309    let run = class_at(text, at, words);
310    while at < len && class_at(text, at, words) == run {
311        at = text.next_cluster_byte(at);
312    }
313    Some(position(text, at))
314}
315
316/// Just past the end of the word at or after `from`.
317///
318/// Distinct from [`word_end_forward`] on purpose. That one is vim's `e`, which
319/// refuses the position it starts on — so from the single-character word in
320/// `"a "` it looks *past* it and finds nothing. This one accepts it, which is
321/// what "delete to the end of the word" means: the caller is naming a range that
322/// starts where the cursor is, not asking where `e` would land.
323///
324/// `None` when there is no word at or after `from`.
325pub fn word_end_at_or_after(text: &Text, from: Position, words: Words) -> Option<Position> {
326    let len = text.len_bytes();
327    let mut at = from.byte();
328    while at < len && class_at(text, at, words) == Class::Blank {
329        at = text.next_cluster_byte(at);
330    }
331    if at >= len {
332        return None;
333    }
334    let run = class_at(text, at, words);
335    while at < len && class_at(text, at, words) == run {
336        at = text.next_cluster_byte(at);
337    }
338    Some(position(text, at))
339}
340
341/// Just past the end of the previous word. Vim's `ge` and `gE`.
342///
343/// `None` when there is no word behind.
344pub fn word_end_back(text: &Text, from: Position, words: Words) -> Option<Position> {
345    // Stated as a search for a place rather than a walk through runs. Stepping
346    // off the cursor and looking for the previous non-blank lands *inside* the
347    // word the cursor is already in, which is not behind it at all.
348    let mut at = text.prev_cluster_byte(from.byte());
349    while at > 0 {
350        if is_word_end(text, at, words) {
351            return Some(position(text, at));
352        }
353        at = text.prev_cluster_byte(at);
354    }
355    None
356}
357
358/// Whether `byte` is just past the end of a word: something non-blank behind it,
359/// and something of a different class at it.
360fn is_word_end(text: &Text, byte: usize, words: Words) -> bool {
361    let behind = class_at(text, text.prev_cluster_byte(byte), words);
362    behind != Class::Blank && class_at(text, byte, words) != behind
363}
364
365// -- visual lines ------------------------------------------------------------
366
367/// Where a vertical motion over *drawn* lines aims to land.
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub enum VisualGoal {
370    /// Land in this cell, counting the row's gutter, or at the line's end if it
371    /// is shorter.
372    Cell(usize),
373    /// Land at the end of the drawn line.
374    LineEnd,
375}
376
377/// `lines` drawn lines down (or up, when negative), aiming at `goal`.
378///
379/// The motion a cursor key should perform in a wrapped editor: one press moves
380/// one drawn line, not past the whole remainder of a soft-wrapped paragraph.
381/// Expressible only because the same crate owns the cursor and the layout —
382/// splitting those two across a library boundary is what made this unfixable
383/// before.
384pub fn visual_vertical(
385    text: &Text,
386    layout: &Layout,
387    hints: &[RowHints<'_>],
388    from: Position,
389    lines: isize,
390    goal: VisualGoal,
391) -> Position {
392    let last = layout.visual_line_count().saturating_sub(1);
393    let current = layout.visual_row_of(from);
394    let row = if lines >= 0 {
395        current.saturating_add(lines.unsigned_abs()).min(last)
396    } else {
397        current.saturating_sub(lines.unsigned_abs())
398    };
399    let column = match goal {
400        VisualGoal::Cell(column) => column,
401        VisualGoal::LineEnd => usize::MAX,
402    };
403    layout
404        .position_at_cell(text, hints, Cell { row, column })
405        .unwrap_or(from)
406}
407
408/// The start of the drawn line the cursor is on.
409///
410/// What Home should do where rows wrap: the start of what the reader sees as this
411/// line, not of the logical row it belongs to.
412pub fn visual_line_start(
413    text: &Text,
414    layout: &Layout,
415    hints: &[RowHints<'_>],
416    from: Position,
417) -> Position {
418    let row = layout.visual_row_of(from);
419    layout
420        .position_at_cell(text, hints, Cell { row, column: 0 })
421        .unwrap_or(from)
422}
423
424/// The end of the drawn line the cursor is on.
425pub fn visual_line_end(
426    text: &Text,
427    layout: &Layout,
428    hints: &[RowHints<'_>],
429    from: Position,
430) -> Position {
431    let row = layout.visual_row_of(from);
432    layout
433        .position_at_cell(
434            text,
435            hints,
436            Cell {
437                row,
438                column: usize::MAX,
439            },
440        )
441        .unwrap_or(from)
442}
443
444// -- searching within a row --------------------------------------------------
445
446/// The next occurrence of `needle` after `from`, on `from`'s row only.
447///
448/// `till` stops just before it rather than on it — vim's `t` against `f`. `None`
449/// when the row holds no further occurrence, which is what makes `dt,` on a row
450/// without a comma do nothing.
451pub fn find_char_forward(
452    text: &Text,
453    from: Position,
454    needle: char,
455    till: bool,
456) -> Option<Position> {
457    let line = text.line(from.row())?;
458    let row_start = text.row_start_byte(from.row());
459    let from_offset = from.byte().saturating_sub(row_start);
460    let mut found = None;
461    for (offset, cluster) in indices(&line) {
462        if offset > from_offset && cluster.starts_with(needle) {
463            found = Some(offset);
464            break;
465        }
466    }
467    let offset = found?;
468    let byte = row_start + offset;
469    Some(position(
470        text,
471        if till {
472            text.prev_cluster_byte(byte)
473        } else {
474            byte
475        },
476    ))
477}
478
479/// The previous occurrence of `needle` before `from`, on `from`'s row only.
480///
481/// `till` stops just after it. Vim's `F` and `T`.
482pub fn find_char_back(text: &Text, from: Position, needle: char, till: bool) -> Option<Position> {
483    let line = text.line(from.row())?;
484    let row_start = text.row_start_byte(from.row());
485    let from_offset = from.byte().saturating_sub(row_start);
486    let mut found = None;
487    for (offset, cluster) in indices(&line) {
488        if offset < from_offset && cluster.starts_with(needle) {
489            found = Some(offset);
490        }
491    }
492    let offset = found?;
493    let byte = row_start + offset;
494    Some(position(
495        text,
496        if till {
497            text.next_cluster_byte(byte)
498        } else {
499            byte
500        },
501    ))
502}
503
504// -- brackets ----------------------------------------------------------------
505
506/// The bracket matching the one at or after the cursor on its row. Vim's `%`.
507///
508/// Scans for the first bracket at or after the cursor on the row — as vim does —
509/// then walks the whole text for its partner, counting nesting. `None` when there
510/// is no bracket ahead on the row, or it is unbalanced.
511pub fn matching_bracket(text: &Text, from: Position) -> Option<Position> {
512    const PAIRS: [(char, char); 3] = [('(', ')'), ('[', ']'), ('{', '}')];
513
514    let len = text.len_bytes();
515    let row_end = row_end(text, from).byte();
516    let mut at = from.byte();
517    let (open, close, forward) = loop {
518        if at >= row_end {
519            return None;
520        }
521        let c = text.scalar_at(at)?;
522        if let Some(&(open, close)) = PAIRS.iter().find(|(open, _)| *open == c) {
523            break (open, close, true);
524        }
525        if let Some(&(open, close)) = PAIRS.iter().find(|(_, close)| *close == c) {
526            break (open, close, false);
527        }
528        at = text.next_cluster_byte(at);
529    };
530
531    let mut depth = 0i32;
532    loop {
533        let c = text.scalar_at(at)?;
534        if c == open {
535            depth += if forward { 1 } else { -1 };
536        } else if c == close {
537            depth += if forward { -1 } else { 1 };
538        }
539        if depth == 0 {
540            return Some(position(text, at));
541        }
542        if forward {
543            at = text.next_cluster_byte(at);
544            if at >= len {
545                return None;
546            }
547        } else {
548            if at == 0 {
549                return None;
550            }
551            at = text.prev_cluster_byte(at);
552        }
553    }
554}
555
556// -- helpers -----------------------------------------------------------------
557
558fn position(text: &Text, byte: usize) -> Position {
559    text.position_at_derived_byte(byte)
560}
561
562fn class_at(text: &Text, byte: usize, words: Words) -> Class {
563    text.scalar_at(byte)
564        .map(|c| words.classify(c))
565        .unwrap_or(Class::Blank)
566}
567
568fn is_blank_row(text: &Text, row: usize) -> bool {
569    text.line(row)
570        .map(|line| line.trim().is_empty())
571        .unwrap_or(true)
572}
573
574/// Whether `byte` is the line break of an otherwise empty row, or the start of
575/// one.
576///
577/// Vim treats an empty line as a word, so a forward or backward word motion stops
578/// there instead of skipping it with the surrounding blanks.
579fn is_empty_row_at(text: &Text, byte: usize) -> bool {
580    let row = text.row_of_byte(byte);
581    text.line_len_chars(row) == Some(0)
582}
583
584fn indices(line: &str) -> impl Iterator<Item = (usize, &str)> {
585    use unicode_segmentation::UnicodeSegmentation;
586    line.grapheme_indices(true)
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592
593    /// "e" plus a combining acute, then "x": three scalars, two clusters.
594    const COMBINING: &str = "e\u{301}x";
595
596    fn text(s: &str) -> Text {
597        Text::from(s)
598    }
599
600    fn at(text: &Text, row: usize, col: usize) -> Position {
601        text.position(row, Column::new(col))
602            .expect("addressable in the test fixture")
603    }
604
605    /// Where a motion landed, as `(row, column)`.
606    fn rc(position: Position) -> (usize, usize) {
607        (position.row(), position.column().get())
608    }
609
610    // -- stepping -----------------------------------------------------------
611
612    #[test]
613    fn stepping_within_a_row_stops_at_its_ends() {
614        let t = text("ab\ncd");
615        assert_eq!(rc(right(&t, at(&t, 0, 2))), (0, 2), "row end holds");
616        assert_eq!(rc(left(&t, at(&t, 1, 0))), (1, 0), "row start holds");
617        assert_eq!(rc(right(&t, at(&t, 0, 1))), (0, 2));
618        assert_eq!(rc(left(&t, at(&t, 0, 1))), (0, 0));
619    }
620
621    #[test]
622    fn stepping_across_rows_crosses_the_break() {
623        let t = text("ab\ncd");
624        assert_eq!(rc(next_cluster(&t, at(&t, 0, 2))), (1, 0));
625        assert_eq!(rc(prev_cluster(&t, at(&t, 1, 0))), (0, 2));
626    }
627
628    #[test]
629    fn stepping_at_the_ends_of_the_text_holds() {
630        let t = text("ab");
631        assert_eq!(rc(next_cluster(&t, at(&t, 0, 2))), (0, 2));
632        assert_eq!(rc(prev_cluster(&t, at(&t, 0, 0))), (0, 0));
633    }
634
635    #[test]
636    fn one_step_crosses_a_whole_cluster() {
637        // Two scalars, one cluster: stepping right from the start must land past
638        // the combining mark, not on it.
639        let t = text(COMBINING);
640        let stepped = right(&t, at(&t, 0, 0));
641        assert_eq!(stepped.column().get(), 2);
642        assert_eq!(rc(left(&t, stepped)), (0, 0));
643    }
644
645    // -- row bounds ---------------------------------------------------------
646
647    #[test]
648    fn row_bounds_ignore_the_line_break() {
649        let t = text("one\ntwo");
650        assert_eq!(rc(row_start(&t, at(&t, 0, 2))), (0, 0));
651        assert_eq!(rc(row_end(&t, at(&t, 0, 1))), (0, 3));
652    }
653
654    #[test]
655    fn non_blank_bounds_skip_the_padding() {
656        let t = text("  padded  ");
657        assert_eq!(rc(first_non_blank(&t, at(&t, 0, 0))), (0, 2));
658        assert_eq!(
659            rc(last_non_blank(&t, at(&t, 0, 0))),
660            (0, 8),
661            "just past the last non-blank, since a position sits between clusters"
662        );
663    }
664
665    #[test]
666    fn an_all_blank_row_has_no_non_blank_to_find() {
667        let t = text("    ");
668        assert_eq!(rc(first_non_blank(&t, at(&t, 0, 0))), (0, 4));
669        assert_eq!(rc(last_non_blank(&t, at(&t, 0, 2))), (0, 0));
670    }
671
672    // -- vertical -----------------------------------------------------------
673
674    #[test]
675    fn a_short_row_does_not_eat_the_goal_column() {
676        let t = text("long enough\nab\nlong enough");
677        let start = at(&t, 0, 9);
678        let goal = Goal::Column(start.column());
679        let middle = vertical(&t, start, 1, goal);
680        assert_eq!(rc(middle), (1, 2), "clamped to the short row");
681        let bottom = vertical(&t, middle, 1, goal);
682        assert_eq!(
683            rc(bottom),
684            (2, 9),
685            "and back out to the column the caller still wants"
686        );
687    }
688
689    #[test]
690    fn aiming_at_the_row_end_follows_the_row() {
691        let t = text("long enough\nab");
692        let landed = vertical(&t, at(&t, 0, 11), 1, Goal::RowEnd);
693        assert_eq!(rc(landed), (1, 2));
694    }
695
696    #[test]
697    fn vertical_saturates_at_the_first_and_last_row() {
698        let t = text("a\nb\nc");
699        let goal = Goal::Column(Column::ZERO);
700        assert_eq!(rc(vertical(&t, at(&t, 0, 0), -3, goal)), (0, 0));
701        assert_eq!(rc(vertical(&t, at(&t, 0, 0), 99, goal)), (2, 0));
702    }
703
704    #[test]
705    fn vertical_will_not_land_inside_a_cluster() {
706        // Row 1's first two scalars are one cluster, so column 1 is not a place.
707        let t = text(&format!("abc\n{COMBINING}"));
708        let landed = vertical(&t, at(&t, 0, 1), 1, Goal::Column(Column::new(1)));
709        assert_eq!(rc(landed), (1, 0));
710    }
711
712    // -- paragraphs ---------------------------------------------------------
713
714    #[test]
715    fn paragraph_motions_stop_on_blank_rows() {
716        let t = text("one\ntwo\n\nthree\n\nfour");
717        assert_eq!(rc(paragraph_forward(&t, at(&t, 0, 0))), (2, 0));
718        assert_eq!(rc(paragraph_forward(&t, at(&t, 3, 0))), (4, 0));
719        assert_eq!(rc(paragraph_back(&t, at(&t, 5, 0))), (4, 0));
720        assert_eq!(rc(paragraph_back(&t, at(&t, 3, 0))), (2, 0));
721    }
722
723    #[test]
724    fn paragraph_motions_saturate_at_the_ends() {
725        let t = text("one\ntwo");
726        assert_eq!(paragraph_forward(&t, at(&t, 0, 0)).byte(), t.len_bytes());
727        assert_eq!(rc(paragraph_back(&t, at(&t, 1, 0))), (0, 0));
728    }
729
730    #[test]
731    fn a_row_of_only_blanks_counts_as_a_paragraph_break() {
732        let t = text("one\n   \ntwo");
733        assert_eq!(rc(paragraph_forward(&t, at(&t, 0, 0))), (1, 0));
734    }
735
736    // -- words --------------------------------------------------------------
737
738    #[test]
739    fn a_small_word_ends_where_the_character_class_changes() {
740        let t = text("foo.bar");
741        assert_eq!(
742            rc(word_start_forward(&t, at(&t, 0, 0), Words::Small)),
743            (0, 3),
744            "the punctuation run is its own word"
745        );
746    }
747
748    #[test]
749    fn a_big_word_is_any_run_of_non_blanks() {
750        let t = text("foo.bar baz");
751        assert_eq!(
752            rc(word_start_forward(&t, at(&t, 0, 0), Words::Big)),
753            (0, 8),
754            "punctuation does not divide a WORD"
755        );
756    }
757
758    #[test]
759    fn a_word_motion_crosses_rows() {
760        let t = text("foo\nbar");
761        assert_eq!(
762            rc(word_start_forward(&t, at(&t, 0, 0), Words::Small)),
763            (1, 0)
764        );
765        assert_eq!(rc(word_start_back(&t, at(&t, 1, 0), Words::Small)), (0, 0));
766    }
767
768    #[test]
769    fn an_empty_row_is_a_word_of_its_own() {
770        // Vim stops on a blank line rather than skipping it with the whitespace
771        // around it.
772        let t = text("a\n\nb");
773        assert_eq!(
774            rc(word_start_forward(&t, at(&t, 0, 0), Words::Small)),
775            (1, 0)
776        );
777        assert_eq!(rc(word_start_back(&t, at(&t, 2, 0), Words::Small)), (1, 0));
778    }
779
780    #[test]
781    fn a_forward_word_motion_saturates_at_the_end_of_the_text() {
782        let t = text("one two");
783        let last = word_start_forward(&t, at(&t, 0, 4), Words::Small);
784        assert_eq!(last.byte(), t.len_bytes());
785    }
786
787    #[test]
788    fn a_backward_word_motion_finds_the_front_of_the_run_it_is_in() {
789        let t = text("one two three");
790        assert_eq!(rc(word_start_back(&t, at(&t, 0, 6), Words::Small)), (0, 4));
791        assert_eq!(rc(word_start_back(&t, at(&t, 0, 4), Words::Small)), (0, 0));
792        assert_eq!(rc(word_start_back(&t, at(&t, 0, 0), Words::Small)), (0, 0));
793    }
794
795    #[test]
796    fn a_word_end_is_just_past_the_last_cluster_of_the_word() {
797        let t = text("one two");
798        assert_eq!(
799            rc(word_end_forward(&t, at(&t, 0, 0), Words::Small).expect("a word ahead")),
800            (0, 3)
801        );
802        assert_eq!(
803            rc(word_end_forward(&t, at(&t, 0, 3), Words::Small).expect("a word ahead")),
804            (0, 7)
805        );
806    }
807
808    #[test]
809    fn a_word_end_with_no_word_ahead_finds_nothing() {
810        // Not "the end of the text": an operator waiting on this must abort, so
811        // `de` at the end of a buffer deletes nothing.
812        let t = text("one   ");
813        assert!(word_end_forward(&t, at(&t, 0, 3), Words::Small).is_none());
814        assert!(word_end_forward(&t, at(&t, 0, 6), Words::Small).is_none());
815    }
816
817    #[test]
818    fn a_word_end_at_or_after_accepts_the_word_the_cursor_is_in() {
819        let t = text("a bb");
820        // `e` looks past the single-character word and finds the next one.
821        assert_eq!(
822            rc(word_end_forward(&t, at(&t, 0, 0), Words::Small).expect("a word ahead")),
823            (0, 4)
824        );
825        // A delete-to-word-end names the word the cursor is in.
826        assert_eq!(
827            rc(word_end_at_or_after(&t, at(&t, 0, 0), Words::Small).expect("a word here")),
828            (0, 1)
829        );
830    }
831
832    #[test]
833    fn a_word_end_at_or_after_skips_leading_blanks() {
834        let t = text("  ab");
835        assert_eq!(
836            rc(word_end_at_or_after(&t, at(&t, 0, 0), Words::Small).expect("a word ahead")),
837            (0, 4)
838        );
839    }
840
841    #[test]
842    fn a_word_end_at_or_after_crosses_rows_to_find_one() {
843        let t = text("\nab");
844        assert_eq!(
845            rc(word_end_at_or_after(&t, at(&t, 0, 0), Words::Small).expect("a word ahead")),
846            (1, 2)
847        );
848    }
849
850    #[test]
851    fn a_word_end_at_or_after_finds_nothing_in_blanks() {
852        let t = text("a   ");
853        assert!(word_end_at_or_after(&t, at(&t, 0, 2), Words::Small).is_none());
854    }
855
856    #[test]
857    fn a_backward_word_end_finds_the_previous_word() {
858        let t = text("one two");
859        assert_eq!(
860            rc(word_end_back(&t, at(&t, 0, 5), Words::Small).expect("a word behind")),
861            (0, 3)
862        );
863        assert!(word_end_back(&t, at(&t, 0, 0), Words::Small).is_none());
864    }
865
866    #[test]
867    fn word_classes_are_the_ones_the_motions_use() {
868        assert_eq!(Words::Small.class_of('a'), Class::Word);
869        assert_eq!(Words::Small.class_of('_'), Class::Word);
870        assert_eq!(Words::Small.class_of('.'), Class::Punctuation);
871        assert_eq!(Words::Big.class_of('.'), Class::Word);
872        assert_eq!(Words::Small.class_of(' '), Class::Blank);
873        assert_eq!(Words::Big.class_of('\n'), Class::Blank);
874    }
875
876    // -- find char ----------------------------------------------------------
877
878    #[test]
879    fn finding_a_character_lands_on_it_or_before_it() {
880        let t = text("hello");
881        assert_eq!(
882            rc(find_char_forward(&t, at(&t, 0, 0), 'l', false).expect("found")),
883            (0, 2)
884        );
885        assert_eq!(
886            rc(find_char_forward(&t, at(&t, 0, 0), 'l', true).expect("found")),
887            (0, 1),
888            "till stops one short"
889        );
890    }
891
892    #[test]
893    fn finding_backwards_takes_the_nearest_one_behind() {
894        let t = text("hello");
895        assert_eq!(
896            rc(find_char_back(&t, at(&t, 0, 4), 'l', false).expect("found")),
897            (0, 3)
898        );
899    }
900
901    #[test]
902    fn finding_a_character_never_leaves_the_row() {
903        let t = text("abc\nxbz");
904        assert!(find_char_forward(&t, at(&t, 0, 0), 'x', false).is_none());
905        assert!(find_char_back(&t, at(&t, 1, 2), 'a', false).is_none());
906    }
907
908    #[test]
909    fn finding_a_character_that_is_not_there_finds_nothing() {
910        let t = text("hello");
911        assert!(find_char_forward(&t, at(&t, 0, 0), 'q', false).is_none());
912        assert!(
913            find_char_forward(&t, at(&t, 0, 4), 'h', false).is_none(),
914            "the search starts past the cursor"
915        );
916    }
917
918    // -- brackets -----------------------------------------------------------
919
920    #[test]
921    fn a_bracket_matches_its_partner() {
922        let t = text("(a[b]c)");
923        assert_eq!(
924            rc(matching_bracket(&t, at(&t, 0, 0)).expect("balanced")),
925            (0, 6),
926            "the inner pair of a different kind is not the partner"
927        );
928    }
929
930    #[test]
931    fn nesting_is_counted() {
932        let t = text("((x))");
933        assert_eq!(
934            rc(matching_bracket(&t, at(&t, 0, 0)).expect("balanced")),
935            (0, 4)
936        );
937        assert_eq!(
938            rc(matching_bracket(&t, at(&t, 0, 1)).expect("balanced")),
939            (0, 3)
940        );
941    }
942
943    #[test]
944    fn a_closing_bracket_matches_backwards() {
945        let t = text("((x))");
946        assert_eq!(
947            rc(matching_bracket(&t, at(&t, 0, 4)).expect("balanced")),
948            (0, 0)
949        );
950    }
951
952    #[test]
953    fn the_search_starts_at_the_first_bracket_on_the_row() {
954        // Vim's `%` from anywhere before a bracket on the row uses that bracket.
955        let t = text("if cond {\n}");
956        assert_eq!(
957            rc(matching_bracket(&t, at(&t, 0, 0)).expect("balanced")),
958            (1, 0)
959        );
960    }
961
962    #[test]
963    fn a_partner_may_be_on_another_row() {
964        let t = text("fn a(\n  b,\n) {}");
965        assert_eq!(
966            rc(matching_bracket(&t, at(&t, 0, 4)).expect("balanced")),
967            (2, 0)
968        );
969    }
970
971    #[test]
972    fn an_unbalanced_bracket_matches_nothing() {
973        let opening = text("(a");
974        assert!(matching_bracket(&opening, opening.start()).is_none());
975        let closing = text("a)");
976        assert!(matching_bracket(&closing, closing.start()).is_none());
977    }
978
979    #[test]
980    fn a_row_without_a_bracket_matches_nothing() {
981        let t = text("plain text\n()");
982        assert!(
983            matching_bracket(&t, at(&t, 0, 0)).is_none(),
984            "the bracket on the next row is not this row's"
985        );
986    }
987
988    #[test]
989    fn a_word_motion_leaves_an_empty_row_it_starts_on() {
990        // The empty-row stop must not apply to the row the cursor is already on,
991        // or the motion tells the cursor to stay where it is.
992        let t = text("a\n\nb");
993        assert_eq!(
994            rc(word_start_forward(&t, at(&t, 1, 0), Words::Small)),
995            (2, 0)
996        );
997    }
998
999    #[test]
1000    fn a_backward_word_end_crosses_a_punctuation_run() {
1001        let t = text("foo.bar");
1002        assert_eq!(
1003            rc(word_end_back(&t, at(&t, 0, 5), Words::Small).expect("a run behind")),
1004            (0, 4),
1005            "the punctuation run's end, not the word before it"
1006        );
1007        assert!(
1008            word_end_back(&t, at(&t, 0, 5), Words::Big).is_none(),
1009            "to a WORD, foo.bar is one run, so no word ends behind the cursor"
1010        );
1011    }
1012
1013    // -- visual lines -------------------------------------------------------
1014
1015    mod visual {
1016        use super::*;
1017        use crate::ropetext::layout::{Layout, RowHints, Viewport};
1018        use crate::ropetext::width::Metrics;
1019
1020        fn layout(text: &Text, width: usize) -> Layout {
1021            Layout::compute(text, width, Metrics::default(), &[])
1022        }
1023
1024        #[test]
1025        fn down_moves_one_drawn_line_not_one_row() {
1026            // The whole point. "aaaa bbbb cccc" wraps into three drawn lines at
1027            // width 5, so pressing down once must stay inside the logical row
1028            // instead of skipping the rest of the paragraph.
1029            let t = text("aaaa bbbb cccc\nnext");
1030            let l = layout(&t, 5);
1031            let start = at(&t, 0, 0);
1032            let one = visual_vertical(&t, &l, &[], start, 1, VisualGoal::Cell(0));
1033            assert_eq!(rc(one), (0, 5), "the second drawn line of the same row");
1034            let two = visual_vertical(&t, &l, &[], one, 1, VisualGoal::Cell(0));
1035            assert_eq!(rc(two), (0, 10));
1036            let three = visual_vertical(&t, &l, &[], two, 1, VisualGoal::Cell(0));
1037            assert_eq!(rc(three), (1, 0), "and only now the next row");
1038        }
1039
1040        #[test]
1041        fn up_and_down_are_symmetric() {
1042            let t = text("aaaa bbbb cccc");
1043            let l = layout(&t, 5);
1044            let middle = at(&t, 0, 5);
1045            assert_eq!(
1046                rc(visual_vertical(
1047                    &t,
1048                    &l,
1049                    &[],
1050                    middle,
1051                    -1,
1052                    VisualGoal::Cell(0)
1053                )),
1054                (0, 0)
1055            );
1056            assert_eq!(
1057                rc(visual_vertical(&t, &l, &[], middle, 1, VisualGoal::Cell(0))),
1058                (0, 10)
1059            );
1060        }
1061
1062        #[test]
1063        fn the_goal_cell_is_kept_across_a_short_drawn_line() {
1064            let t = text("aaaaa\nb\nccccc");
1065            let l = layout(&t, 10);
1066            let start = at(&t, 0, 4);
1067            let goal = VisualGoal::Cell(4);
1068            let middle = visual_vertical(&t, &l, &[], start, 1, goal);
1069            assert_eq!(rc(middle), (1, 1), "clamped to the short row");
1070            let bottom = visual_vertical(&t, &l, &[], middle, 1, goal);
1071            assert_eq!(rc(bottom), (2, 4), "and back out to the cell still wanted");
1072        }
1073
1074        #[test]
1075        fn aiming_at_the_line_end_follows_the_wrap() {
1076            let t = text("aaaa bbbb\nxy");
1077            let l = layout(&t, 5);
1078            let landed = visual_vertical(&t, &l, &[], at(&t, 0, 0), 1, VisualGoal::LineEnd);
1079            assert_eq!(rc(landed), (0, 9), "the end of the second drawn line");
1080        }
1081
1082        #[test]
1083        fn vertical_movement_saturates_at_the_first_and_last_drawn_line() {
1084            let t = text("aaaa bbbb");
1085            let l = layout(&t, 5);
1086            let goal = VisualGoal::Cell(0);
1087            assert_eq!(
1088                rc(visual_vertical(&t, &l, &[], at(&t, 0, 0), -9, goal)),
1089                (0, 0)
1090            );
1091            assert_eq!(
1092                rc(visual_vertical(&t, &l, &[], at(&t, 0, 0), 9, goal)),
1093                (0, 5)
1094            );
1095        }
1096
1097        #[test]
1098        fn line_bounds_are_the_drawn_line_not_the_row() {
1099            let t = text("aaaa bbbb cccc");
1100            let l = layout(&t, 5);
1101            let inside = at(&t, 0, 7);
1102            assert_eq!(rc(visual_line_start(&t, &l, &[], inside)), (0, 5));
1103            assert_eq!(rc(visual_line_end(&t, &l, &[], inside)), (0, 9));
1104            // The logical row's bounds are elsewhere, and both are wanted: Home in a
1105            // wrapped editor means the drawn line, `0` in vim means the row.
1106            assert_eq!(rc(row_start(&t, inside)), (0, 0));
1107            assert_eq!(rc(row_end(&t, inside)), (0, 14));
1108        }
1109
1110        #[test]
1111        fn a_gutter_shifts_the_cells_a_visual_motion_aims_at() {
1112            let t = text("abcd\nefgh");
1113            let hints = [
1114                RowHints {
1115                    visible: &[],
1116                    inset: 2,
1117                },
1118                RowHints {
1119                    visible: &[],
1120                    inset: 2,
1121                },
1122            ];
1123            let l = Layout::compute(&t, 10, Metrics::default(), &hints);
1124            let landed = visual_vertical(&t, &l, &hints, at(&t, 0, 0), 1, VisualGoal::Cell(3));
1125            assert_eq!(
1126                rc(landed),
1127                (1, 1),
1128                "cell 3 is the second character past a two-cell gutter"
1129            );
1130        }
1131
1132        #[test]
1133        fn a_viewport_can_follow_a_visual_motion() {
1134            let t = text("aaaa bbbb cccc dddd");
1135            let l = layout(&t, 5);
1136            let mut view = Viewport::new(2);
1137            let mut position = at(&t, 0, 0);
1138            for _ in 0..3 {
1139                position = visual_vertical(&t, &l, &[], position, 1, VisualGoal::Cell(0));
1140                view.follow(&l, position);
1141            }
1142            assert_eq!(view.top(), 2, "scrolled by drawn lines, not by rows");
1143        }
1144    }
1145
1146    // -- properties ---------------------------------------------------------
1147
1148    mod properties {
1149        use super::*;
1150        use proptest::prelude::*;
1151
1152        fn addressable(t: &Text) -> Vec<Position> {
1153            (0..=t.len_bytes())
1154                .filter_map(|byte| t.position_at_byte(byte))
1155                .collect()
1156        }
1157
1158        /// Every motion, from every addressable place.
1159        fn all(t: &Text, from: Position) -> Vec<Position> {
1160            let mut landed = vec![
1161                right(t, from),
1162                left(t, from),
1163                next_cluster(t, from),
1164                prev_cluster(t, from),
1165                row_start(t, from),
1166                row_end(t, from),
1167                first_non_blank(t, from),
1168                last_non_blank(t, from),
1169                paragraph_forward(t, from),
1170                paragraph_back(t, from),
1171                goto_row(t, from.row()),
1172                vertical(t, from, 1, Goal::Column(from.column())),
1173                vertical(t, from, -1, Goal::RowEnd),
1174            ];
1175            for words in [Words::Small, Words::Big] {
1176                landed.push(word_start_forward(t, from, words));
1177                landed.push(word_start_back(t, from, words));
1178                landed.extend(word_end_forward(t, from, words));
1179                landed.extend(word_end_at_or_after(t, from, words));
1180                landed.extend(word_end_back(t, from, words));
1181            }
1182            landed.extend(matching_bracket(t, from));
1183            landed.extend(find_char_forward(t, from, 'a', false));
1184            landed.extend(find_char_forward(t, from, 'a', true));
1185            landed.extend(find_char_back(t, from, 'a', false));
1186            landed.extend(find_char_back(t, from, 'a', true));
1187            landed
1188        }
1189
1190        proptest! {
1191            // Every case walks every motion from every boundary, so the case count
1192            // is kept low deliberately: the cost is quadratic in the corpus and
1193            // the interesting shapes are small.
1194            #![proptest_config(ProptestConfig::with_cases(64))]
1195
1196            /// No motion can produce a position the text cannot address. This is
1197            /// what stops `position_at_derived_byte`'s assertion from being the
1198            /// thing that finds it, in a user's note.
1199            #[test]
1200            fn every_motion_lands_somewhere_addressable(s in ".{0,40}") {
1201                let t = Text::from(s.as_str());
1202                for from in addressable(&t) {
1203                    for landed in all(&t, from) {
1204                        prop_assert!(
1205                            t.position_at_byte(landed.byte()).is_some(),
1206                            "byte {} of {:?} is not addressable", landed.byte(), s
1207                        );
1208                        prop_assert!(!t.is_stale(landed));
1209                        prop_assert_eq!(
1210                            t.position_at_byte(landed.byte()),
1211                            Some(landed),
1212                            "row and column disagree with the byte"
1213                        );
1214                    }
1215                }
1216            }
1217
1218            /// Forward motions never go backwards, and backward ones never
1219            /// forwards. A motion that overshoots into the other direction is how
1220            /// an operator range comes out inverted.
1221            #[test]
1222            fn motions_keep_their_direction(s in ".{0,40}") {
1223                let t = Text::from(s.as_str());
1224                for from in addressable(&t) {
1225                    for words in [Words::Small, Words::Big] {
1226                        prop_assert!(word_start_forward(&t, from, words).byte() >= from.byte());
1227                        prop_assert!(word_start_back(&t, from, words).byte() <= from.byte());
1228                        if let Some(end) = word_end_forward(&t, from, words) {
1229                            prop_assert!(end.byte() > from.byte());
1230                        }
1231                        if let Some(end) = word_end_at_or_after(&t, from, words) {
1232                            prop_assert!(end.byte() > from.byte());
1233                        }
1234                        if let Some(end) = word_end_back(&t, from, words) {
1235                            prop_assert!(end.byte() < from.byte());
1236                        }
1237                    }
1238                    prop_assert!(right(&t, from).byte() >= from.byte());
1239                    prop_assert!(left(&t, from).byte() <= from.byte());
1240                    prop_assert!(next_cluster(&t, from).byte() >= from.byte());
1241                    prop_assert!(prev_cluster(&t, from).byte() <= from.byte());
1242                    prop_assert!(paragraph_forward(&t, from).byte() >= from.byte());
1243                    prop_assert!(paragraph_back(&t, from).byte() <= from.byte());
1244                }
1245            }
1246
1247            /// Walking by words reaches the end of the text and stops there. A
1248            /// motion that returns where it started is a motion the caller can
1249            /// loop on forever.
1250            #[test]
1251            fn walking_by_words_terminates(s in ".{0,60}") {
1252                let t = Text::from(s.as_str());
1253                for words in [Words::Small, Words::Big] {
1254                    let mut at = t.start();
1255                    let mut steps = 0;
1256                    loop {
1257                        let next = word_start_forward(&t, at, words);
1258                        if next.byte() == at.byte() {
1259                            break;
1260                        }
1261                        at = next;
1262                        steps += 1;
1263                        prop_assert!(steps <= t.len_bytes() + 2, "no progress in {:?}", s);
1264                    }
1265                    prop_assert_eq!(at.byte(), t.len_bytes(), "stopped short in {:?}", s);
1266                }
1267            }
1268        }
1269    }
1270}