floem/views/editor/
movement.rs

1//! Movement logic for the editor.
2
3use floem_editor_core::{
4    buffer::rope_text::{RopeText, RopeTextVal},
5    command::MultiSelectionCommand,
6    cursor::{ColPosition, Cursor, CursorAffinity, CursorMode},
7    mode::{Mode, MotionMode, VisualMode},
8    movement::{LinePosition, Movement},
9    register::Register,
10    selection::{SelRegion, Selection},
11    soft_tab::{SnapDirection, snap_to_soft_tab},
12};
13
14use super::{
15    Editor,
16    actions::CommonAction,
17    visual_line::{RVLine, VLineInfo},
18};
19
20/// Move a selection region by a given movement.
21///
22/// Much of the time, this will just be a matter of moving the cursor, but
23/// some movements may depend on the current selection.
24fn move_region(
25    view: &Editor,
26    region: &SelRegion,
27    count: usize,
28    modify: bool,
29    movement: &Movement,
30    mode: Mode,
31) -> SelRegion {
32    let (count, region) = if count >= 1 && !modify && !region.is_caret() {
33        // If we're not a caret, and we are moving left/up or right/down, we want to move
34        // the cursor to the left or right side of the selection.
35        // Ex: `|abc|` -> left/up arrow key -> `|abc`
36        // Ex: `|abc|` -> right/down arrow key -> `abc|`
37        // and it doesn't matter which direction the selection is going, so we use min/max
38        match movement {
39            Movement::Left | Movement::Up => {
40                let leftmost = region.min();
41                (
42                    count - 1,
43                    SelRegion::new(leftmost, leftmost, region.affinity, region.horiz),
44                )
45            }
46            Movement::Right | Movement::Down => {
47                let rightmost = region.max();
48                (
49                    count - 1,
50                    SelRegion::new(rightmost, rightmost, region.affinity, region.horiz),
51                )
52            }
53            _ => (count, *region),
54        }
55    } else {
56        (count, *region)
57    };
58
59    let mut affinity = region.affinity;
60
61    let (end, horiz) = move_offset(
62        view,
63        region.end,
64        region.horiz.as_ref(),
65        &mut affinity,
66        count,
67        movement,
68        mode,
69    );
70    let start = match modify {
71        true => region.start,
72        false => end,
73    };
74    SelRegion::new(start, end, affinity, horiz)
75}
76
77pub fn move_selection(
78    view: &Editor,
79    selection: &Selection,
80    count: usize,
81    modify: bool,
82    movement: &Movement,
83    mode: Mode,
84) -> Selection {
85    let mut new_selection = Selection::new();
86    for region in selection.regions() {
87        new_selection.add_region(move_region(view, region, count, modify, movement, mode));
88    }
89    new_selection
90}
91
92// TODO: It would probably fit the overall logic better if affinity was immutable and it just returned the new affinity!
93pub fn move_offset(
94    view: &Editor,
95    offset: usize,
96    horiz: Option<&ColPosition>,
97    affinity: &mut CursorAffinity,
98    count: usize,
99    movement: &Movement,
100    mode: Mode,
101) -> (usize, Option<ColPosition>) {
102    let (new_offset, horiz) = match movement {
103        Movement::Left => {
104            let new_offset = move_left(view, offset, affinity, mode, count);
105
106            (new_offset, None)
107        }
108        Movement::Right => {
109            let new_offset = move_right(view, offset, affinity, mode, count);
110
111            (new_offset, None)
112        }
113        Movement::Up => {
114            let (new_offset, horiz) = move_up(view, offset, affinity, horiz.cloned(), mode, count);
115
116            (new_offset, Some(horiz))
117        }
118        Movement::Down => {
119            let (new_offset, horiz) =
120                move_down(view, offset, affinity, horiz.cloned(), mode, count);
121
122            (new_offset, Some(horiz))
123        }
124        Movement::DocumentStart => {
125            // Put it before any inlay hints at the very start
126            *affinity = CursorAffinity::Backward;
127            (0, Some(ColPosition::Start))
128        }
129        Movement::DocumentEnd => {
130            let (new_offset, horiz) = document_end(view.rope_text(), affinity, mode);
131
132            (new_offset, Some(horiz))
133        }
134        Movement::FirstNonBlank => {
135            let (new_offset, horiz) = first_non_blank(view, affinity, offset);
136
137            (new_offset, Some(horiz))
138        }
139        Movement::StartOfLine => {
140            let (new_offset, horiz) = start_of_line(view, affinity, offset);
141
142            (new_offset, Some(horiz))
143        }
144        Movement::EndOfLine => {
145            let (new_offset, horiz) = end_of_line(view, affinity, offset, mode);
146
147            (new_offset, Some(horiz))
148        }
149        Movement::Line(position) => {
150            let (new_offset, horiz) = to_line(view, offset, horiz.cloned(), mode, position);
151
152            (new_offset, Some(horiz))
153        }
154        Movement::Offset(offset) => {
155            let new_offset = view.text().prev_grapheme_offset(*offset + 1).unwrap();
156            (new_offset, None)
157        }
158        Movement::WordEndForward => {
159            let new_offset =
160                view.rope_text()
161                    .move_n_wordends_forward(offset, count, mode == Mode::Insert);
162            (new_offset, None)
163        }
164        Movement::WordForward => {
165            let new_offset = view.rope_text().move_n_words_forward(offset, count);
166            (new_offset, None)
167        }
168        Movement::WordBackward => {
169            let new_offset = view.rope_text().move_n_words_backward(offset, count, mode);
170            (new_offset, None)
171        }
172        Movement::NextUnmatched(char) => {
173            let new_offset = view.doc().find_unmatched(offset, false, *char);
174
175            (new_offset, None)
176        }
177        Movement::PreviousUnmatched(char) => {
178            let new_offset = view.doc().find_unmatched(offset, true, *char);
179
180            (new_offset, None)
181        }
182        Movement::MatchPairs => {
183            let new_offset = view.doc().find_matching_pair(offset);
184
185            (new_offset, None)
186        }
187        Movement::ParagraphForward => {
188            let new_offset = view.rope_text().move_n_paragraphs_forward(offset, count);
189
190            (new_offset, None)
191        }
192        Movement::ParagraphBackward => {
193            let new_offset = view.rope_text().move_n_paragraphs_backward(offset, count);
194
195            (new_offset, None)
196        }
197    };
198
199    let new_offset = correct_crlf(&view.rope_text(), new_offset);
200
201    (new_offset, horiz)
202}
203
204/// If the offset is at `\r|\n` then move it back.
205fn correct_crlf(text: &RopeTextVal, offset: usize) -> usize {
206    if offset == 0 || offset == text.len() {
207        return offset;
208    }
209
210    let mut cursor = lapce_xi_rope::Cursor::new(text.text(), offset);
211    if cursor.peek_next_codepoint() == Some('\n') && cursor.prev_codepoint() == Some('\r') {
212        return offset - 1;
213    }
214
215    offset
216}
217
218fn atomic_soft_tab_width_for_offset(ed: &Editor, offset: usize) -> Option<usize> {
219    let line = ed.line_of_offset(offset);
220    let style = ed.style();
221    if style.atomic_soft_tabs(ed.id(), line) {
222        Some(style.tab_width(ed.id(), line))
223    } else {
224        None
225    }
226}
227
228/// Move the offset to the left by `count` amount.
229///
230/// If `soft_tab_width` is `Some` (and greater than 1) then the offset will snap to the soft tab.
231fn move_left(
232    ed: &Editor,
233    offset: usize,
234    affinity: &mut CursorAffinity,
235    mode: Mode,
236    count: usize,
237) -> usize {
238    let rope_text = ed.rope_text();
239    let mut new_offset = rope_text.move_left(offset, mode, count);
240
241    if let Some(soft_tab_width) = atomic_soft_tab_width_for_offset(ed, offset) {
242        if soft_tab_width > 1 {
243            new_offset = snap_to_soft_tab(
244                rope_text.text(),
245                new_offset,
246                SnapDirection::Left,
247                soft_tab_width,
248            );
249        }
250    }
251
252    *affinity = CursorAffinity::Forward;
253
254    new_offset
255}
256
257/// Move the offset to the right by `count` amount.
258/// If `soft_tab_width` is `Some` (and greater than 1) then the offset will snap to the soft tab.
259fn move_right(
260    view: &Editor,
261    offset: usize,
262    affinity: &mut CursorAffinity,
263    mode: Mode,
264    count: usize,
265) -> usize {
266    let rope_text = view.rope_text();
267    let mut new_offset = rope_text.move_right(offset, mode, count);
268
269    if let Some(soft_tab_width) = atomic_soft_tab_width_for_offset(view, offset) {
270        if soft_tab_width > 1 {
271            new_offset = snap_to_soft_tab(
272                rope_text.text(),
273                new_offset,
274                SnapDirection::Right,
275                soft_tab_width,
276            );
277        }
278    }
279
280    *affinity = CursorAffinity::Backward;
281
282    new_offset
283}
284
285fn find_prev_rvline(view: &Editor, start: RVLine, count: usize) -> Option<RVLine> {
286    if count == 0 {
287        return Some(start);
288    }
289
290    // We can't just directly subtract count because of multi-line phantom text.
291    // As just subtracting count wouldn't properly skip over the phantom lines.
292    // So we have to search backwards for the previous line that has real content.
293    let mut info = None;
294    let mut found_count = 0;
295    for prev_info in view.iter_rvlines(true, start).skip(1) {
296        if prev_info.is_empty_phantom() {
297            // We skip any phantom text lines in our consideration
298            continue;
299        }
300
301        // Otherwise we found a real line.
302        found_count += 1;
303
304        if found_count == count {
305            // If we've completed all the count instances then we're done
306            info = Some(prev_info);
307            break;
308        }
309        // Otherwise we continue on to find the previous line with content before that.
310    }
311
312    info.map(|info| info.rvline)
313}
314
315/// Move the offset up by `count` amount.
316///
317/// `count` may be zero, because moving up in a selection just jumps to the start of the selection.
318fn move_up(
319    view: &Editor,
320    offset: usize,
321    affinity: &mut CursorAffinity,
322    horiz: Option<ColPosition>,
323    mode: Mode,
324    count: usize,
325) -> (usize, ColPosition) {
326    let rvline = view.rvline_of_offset(offset, *affinity);
327    if rvline.line == 0 && rvline.line_index == 0 {
328        // Zeroth line
329        let horiz = horiz
330            .unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x));
331
332        *affinity = CursorAffinity::Backward;
333
334        return (0, horiz);
335    }
336
337    let Some(rvline) = find_prev_rvline(view, rvline, count) else {
338        // Zeroth line
339        let horiz = horiz
340            .unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x));
341
342        *affinity = CursorAffinity::Backward;
343
344        return (0, horiz);
345    };
346
347    let horiz =
348        horiz.unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x));
349    let col = view.rvline_horiz_col(rvline, &horiz, mode != Mode::Normal);
350    let new_offset = view.offset_of_line_col(rvline.line, col);
351
352    let info = view.rvline_info(rvline);
353
354    *affinity = if new_offset == info.interval.start {
355        CursorAffinity::Forward
356    } else {
357        CursorAffinity::Backward
358    };
359
360    (new_offset, horiz)
361}
362
363/// Move down for when the cursor is on the last visual line.
364fn move_down_last_rvline(
365    view: &Editor,
366    offset: usize,
367    affinity: &mut CursorAffinity,
368    horiz: Option<ColPosition>,
369    mode: Mode,
370) -> (usize, ColPosition) {
371    let rope_text = view.rope_text();
372
373    let last_line = rope_text.last_line();
374    let new_offset = rope_text.line_end_offset(last_line, mode != Mode::Normal);
375
376    // We should appear after any phantom text at the very end of the line.
377    *affinity = CursorAffinity::Forward;
378
379    let horiz =
380        horiz.unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x));
381
382    (new_offset, horiz)
383}
384
385fn find_next_rvline_info(
386    view: &Editor,
387    offset: usize,
388    start: RVLine,
389    count: usize,
390) -> Option<VLineInfo<()>> {
391    // We can't just directly add count because of multi-line phantom text.
392    // These lines are 'not there' and also don't have any position that can be moved into
393    // (unlike phantom text that is mixed with real text)
394    // So we have to search forward for the next line that has real content.
395    // The typical iteration count for this is 1, and even after that it is usually only a handful.
396    let mut found_count = 0;
397    for next_info in view.iter_rvlines(false, start) {
398        if count == 0 {
399            return Some(next_info);
400        }
401
402        if next_info.is_empty_phantom() {
403            // We skip any phantom text lines in our consideration
404            // TODO: Would this skip over an empty line?
405            continue;
406        }
407
408        if next_info.interval.start < offset || next_info.rvline == start {
409            // If we're on or before our current visual line then we skip it
410            continue;
411        }
412
413        // Otherwise we found a real line.
414        found_count += 1;
415
416        if found_count == count {
417            // If we've completed all the count instances then we're done
418            return Some(next_info);
419        }
420        // Otherwise we continue on to find the next line with content after that.
421    }
422
423    None
424}
425
426/// Move the offset down by `count` amount.
427///
428/// `count` may be zero, because moving down in a selection just jumps to the end of the selection.
429fn move_down(
430    view: &Editor,
431    offset: usize,
432    affinity: &mut CursorAffinity,
433    horiz: Option<ColPosition>,
434    mode: Mode,
435    count: usize,
436) -> (usize, ColPosition) {
437    let rvline = view.rvline_of_offset(offset, *affinity);
438
439    let Some(info) = find_next_rvline_info(view, offset, rvline, count) else {
440        // There was no next entry, this typically means that we would go past the end if we went
441        // further
442        return move_down_last_rvline(view, offset, affinity, horiz, mode);
443    };
444
445    // TODO(minor): is this the right affinity?
446    let horiz =
447        horiz.unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x));
448
449    let col = view.rvline_horiz_col(info.rvline, &horiz, mode != Mode::Normal);
450
451    let new_offset = view.offset_of_line_col(info.rvline.line, col);
452
453    *affinity = if new_offset == info.interval.start {
454        // The column was zero so we shift it to be at the line itself.
455        // This lets us move down to an empty - for example - next line and appear at the
456        // start of that line without coinciding with the offset at the end of the previous line.
457        CursorAffinity::Forward
458    } else {
459        CursorAffinity::Backward
460    };
461
462    (new_offset, horiz)
463}
464
465fn document_end(
466    rope_text: impl RopeText,
467    affinity: &mut CursorAffinity,
468    mode: Mode,
469) -> (usize, ColPosition) {
470    let last_offset = rope_text.offset_line_end(rope_text.len(), mode != Mode::Normal);
471
472    // Put it past any inlay hints directly at the end
473    *affinity = CursorAffinity::Forward;
474
475    (last_offset, ColPosition::End)
476}
477
478fn first_non_blank(
479    view: &Editor,
480    affinity: &mut CursorAffinity,
481    offset: usize,
482) -> (usize, ColPosition) {
483    let info = view.rvline_info_of_offset(offset, *affinity);
484    let non_blank_offset = info.first_non_blank_character(&view.text_prov());
485    let start_line_offset = info.interval.start;
486    // TODO: is this always the correct affinity? It might be desirable for the very first character on a wrapped line?
487    *affinity = CursorAffinity::Forward;
488
489    if offset > non_blank_offset {
490        // Jump to the first non-whitespace character if we're strictly after it
491        (non_blank_offset, ColPosition::FirstNonBlank)
492    } else {
493        // If we're at the start of the line, also jump to the first not blank
494        if start_line_offset == offset {
495            (non_blank_offset, ColPosition::FirstNonBlank)
496        } else {
497            // Otherwise, jump to the start of the line
498            (start_line_offset, ColPosition::Start)
499        }
500    }
501}
502
503fn start_of_line(
504    view: &Editor,
505    affinity: &mut CursorAffinity,
506    offset: usize,
507) -> (usize, ColPosition) {
508    let rvline = view.rvline_of_offset(offset, *affinity);
509    let new_offset = view.offset_of_rvline(rvline);
510    // TODO(minor): if the line has zero characters, it should probably be forward affinity but
511    // other cases might be better as backwards?
512    *affinity = CursorAffinity::Forward;
513
514    (new_offset, ColPosition::Start)
515}
516
517fn end_of_line(
518    view: &Editor,
519    affinity: &mut CursorAffinity,
520    offset: usize,
521    mode: Mode,
522) -> (usize, ColPosition) {
523    let info = view.rvline_info_of_offset(offset, *affinity);
524    let new_col = info.last_col(&view.text_prov(), mode != Mode::Normal);
525    *affinity = if new_col == 0 {
526        CursorAffinity::Forward
527    } else {
528        CursorAffinity::Backward
529    };
530
531    let new_offset = view.offset_of_line_col(info.rvline.line, new_col);
532
533    (new_offset, ColPosition::End)
534}
535
536fn to_line(
537    view: &Editor,
538    offset: usize,
539    horiz: Option<ColPosition>,
540    mode: Mode,
541    position: &LinePosition,
542) -> (usize, ColPosition) {
543    let rope_text = view.rope_text();
544
545    // TODO(minor): Should this use rvline?
546    let line = match position {
547        LinePosition::Line(line) => (line - 1).min(rope_text.last_line()),
548        LinePosition::First => 0,
549        LinePosition::Last => rope_text.last_line(),
550    };
551    // TODO(minor): is this the best affinity?
552    let horiz = horiz.unwrap_or_else(|| {
553        ColPosition::Col(
554            view.line_point_of_offset(offset, CursorAffinity::Backward)
555                .x,
556        )
557    });
558    let col = view.line_horiz_col(line, &horiz, mode != Mode::Normal);
559    let new_offset = rope_text.offset_of_line_col(line, col);
560
561    (new_offset, horiz)
562}
563
564/// Move the current cursor.
565///
566/// This will signal-update the document for some motion modes.
567pub fn move_cursor(
568    ed: &Editor,
569    action: &dyn CommonAction,
570    cursor: &mut Cursor,
571    movement: &Movement,
572    count: usize,
573    modify: bool,
574    register: &mut Register,
575) {
576    match cursor.mode {
577        CursorMode::Normal {
578            offset,
579            mut affinity,
580        } => {
581            let count = if let Some(motion_mode) = cursor.motion_mode.as_ref() {
582                count.max(motion_mode.count())
583            } else {
584                count
585            };
586            let (new_offset, horiz) = move_offset(
587                ed,
588                offset,
589                cursor.horiz.as_ref(),
590                &mut affinity,
591                count,
592                movement,
593                Mode::Normal,
594            );
595            if let Some(motion_mode) = cursor.motion_mode.clone() {
596                let (moved_new_offset, _) = move_offset(
597                    ed,
598                    new_offset,
599                    None,
600                    &mut affinity,
601                    1,
602                    &Movement::Right,
603                    Mode::Insert,
604                );
605                let range = match movement {
606                    Movement::EndOfLine | Movement::WordEndForward => offset..moved_new_offset,
607                    Movement::MatchPairs => {
608                        if new_offset > offset {
609                            offset..moved_new_offset
610                        } else {
611                            moved_new_offset..new_offset
612                        }
613                    }
614                    _ => offset..new_offset,
615                };
616                action.exec_motion_mode(
617                    ed,
618                    cursor,
619                    motion_mode,
620                    range,
621                    movement.is_vertical(),
622                    register,
623                );
624                cursor.motion_mode = None;
625            } else {
626                cursor.mode = CursorMode::Normal {
627                    offset: new_offset,
628                    affinity,
629                };
630                cursor.horiz = horiz;
631            }
632        }
633        CursorMode::Visual {
634            start,
635            end,
636            mode,
637            mut affinity,
638        } => {
639            let (new_offset, horiz) = move_offset(
640                ed,
641                end,
642                cursor.horiz.as_ref(),
643                &mut affinity,
644                count,
645                movement,
646                Mode::Visual(VisualMode::Normal),
647            );
648            cursor.mode = CursorMode::Visual {
649                start,
650                end: new_offset,
651                mode,
652                affinity,
653            };
654            cursor.horiz = horiz;
655        }
656        CursorMode::Insert(ref selection) => {
657            let selection = move_selection(ed, selection, count, modify, movement, Mode::Insert);
658            cursor.set_insert(selection);
659        }
660    }
661}
662
663pub fn do_multi_selection(view: &Editor, cursor: &mut Cursor, cmd: &MultiSelectionCommand) {
664    use MultiSelectionCommand::*;
665    let rope_text = view.rope_text();
666
667    match cmd {
668        SelectUndo => {
669            if let CursorMode::Insert(_) = cursor.mode.clone() {
670                if let Some(selection) = cursor.history_selections.last().cloned() {
671                    cursor.mode = CursorMode::Insert(selection);
672                }
673                cursor.history_selections.pop();
674            }
675        }
676        InsertCursorAbove => {
677            if let CursorMode::Insert(mut selection) = cursor.mode.clone() {
678                let (offset, mut affinity) = selection
679                    .first()
680                    .map(|s| (s.end, s.affinity))
681                    .unwrap_or((0, CursorAffinity::Backward));
682
683                let (new_offset, _) = move_offset(
684                    view,
685                    offset,
686                    cursor.horiz.as_ref(),
687                    &mut affinity,
688                    1,
689                    &Movement::Up,
690                    Mode::Insert,
691                );
692                if new_offset != offset {
693                    selection.add_region(SelRegion::new(new_offset, new_offset, affinity, None));
694                }
695                cursor.set_insert(selection);
696            }
697        }
698        InsertCursorBelow => {
699            if let CursorMode::Insert(mut selection) = cursor.mode.clone() {
700                let (offset, mut affinity) = selection
701                    .last()
702                    .map(|s| (s.end, s.affinity))
703                    .unwrap_or((0, CursorAffinity::Backward));
704
705                let (new_offset, _) = move_offset(
706                    view,
707                    offset,
708                    cursor.horiz.as_ref(),
709                    &mut affinity,
710                    1,
711                    &Movement::Down,
712                    Mode::Insert,
713                );
714                if new_offset != offset {
715                    selection.add_region(SelRegion::new(new_offset, new_offset, affinity, None));
716                }
717                cursor.set_insert(selection);
718            }
719        }
720        InsertCursorEndOfLine => {
721            if let CursorMode::Insert(selection) = cursor.mode.clone() {
722                let mut new_selection = Selection::new();
723                for region in selection.regions() {
724                    let (start_line, _) = rope_text.offset_to_line_col(region.min());
725                    let (end_line, end_col) = rope_text.offset_to_line_col(region.max());
726                    for line in start_line..end_line + 1 {
727                        let offset = if line == end_line {
728                            rope_text.offset_of_line_col(line, end_col)
729                        } else {
730                            rope_text.line_end_offset(line, true)
731                        };
732                        new_selection.add_region(SelRegion::new(
733                            offset,
734                            offset,
735                            CursorAffinity::Backward,
736                            None,
737                        ));
738                    }
739                }
740                cursor.set_insert(new_selection);
741            }
742        }
743        SelectCurrentLine => {
744            if let CursorMode::Insert(selection) = cursor.mode.clone() {
745                let mut new_selection = Selection::new();
746                for region in selection.regions() {
747                    let start_line = rope_text.line_of_offset(region.min());
748                    let start = rope_text.offset_of_line(start_line);
749                    let end_line = rope_text.line_of_offset(region.max());
750                    let end = rope_text.offset_of_line(end_line + 1);
751                    new_selection.add_region(SelRegion::new(
752                        start,
753                        end,
754                        CursorAffinity::Backward,
755                        None,
756                    ));
757                }
758                cursor.set_insert(new_selection);
759            }
760        }
761        SelectAllCurrent | SelectNextCurrent | SelectSkipCurrent => {
762            // TODO: How should we handle these?
763            // The specific common editor behavior is to use the editor's find
764            // to do these finds and use it for the selections.
765            // However, we haven't included a `find` in floem-editor
766        }
767        SelectAll => {
768            let new_selection = Selection::region(0, rope_text.len(), CursorAffinity::Forward);
769            cursor.set_insert(new_selection);
770        }
771    }
772}
773
774pub fn do_motion_mode(
775    ed: &Editor,
776    action: &dyn CommonAction,
777    cursor: &mut Cursor,
778    motion_mode: MotionMode,
779    register: &mut Register,
780) {
781    if let Some(cached_motion_mode) = cursor.motion_mode.take() {
782        // If it's the same MotionMode discriminant, continue, count is cached in the old motion_mode.
783        if core::mem::discriminant(&cached_motion_mode) == core::mem::discriminant(&motion_mode) {
784            let offset = cursor.offset();
785            action.exec_motion_mode(
786                ed,
787                cursor,
788                cached_motion_mode,
789                offset..offset,
790                true,
791                register,
792            );
793        }
794    } else {
795        cursor.motion_mode = Some(motion_mode);
796    }
797}
798
799#[cfg(test)]
800mod tests {
801    use std::rc::Rc;
802
803    use floem_editor_core::{
804        buffer::rope_text::{RopeText, RopeTextVal},
805        cursor::{ColPosition, CursorAffinity},
806        mode::Mode,
807    };
808    use floem_reactive::{Scope, SignalUpdate};
809    use lapce_xi_rope::Rope;
810    use peniko::kurbo::{Rect, Size};
811
812    use crate::views::editor::{
813        movement::{correct_crlf, end_of_line, move_down, move_up},
814        text::SimpleStyling,
815        text_document::TextDocument,
816    };
817
818    use super::Editor;
819
820    fn make_ed(text: &str) -> Editor {
821        let cx = Scope::new();
822        let doc = Rc::new(TextDocument::new(cx, text));
823        let style = Rc::new(SimpleStyling::new());
824        let editor = Editor::new(cx, doc, style, false);
825        editor
826            .viewport
827            .set(Rect::ZERO.with_size(Size::new(f64::MAX, f64::MAX)));
828        editor
829    }
830
831    // Tests for movement logic.
832    // Many of the locations that use affinity are unsure of the specifics, and should only be
833    // assumed to be mostly kinda correct.
834
835    #[test]
836    fn test_correct_crlf() {
837        let text = Rope::from("hello\nworld");
838        let text = RopeTextVal::new(text);
839        assert_eq!(correct_crlf(&text, 0), 0);
840        assert_eq!(correct_crlf(&text, 5), 5);
841        assert_eq!(correct_crlf(&text, 6), 6);
842        assert_eq!(correct_crlf(&text, text.len()), text.len());
843
844        let text = Rope::from("hello\r\nworld");
845        let text = RopeTextVal::new(text);
846        assert_eq!(correct_crlf(&text, 0), 0);
847        assert_eq!(correct_crlf(&text, 5), 5);
848        assert_eq!(correct_crlf(&text, 6), 5);
849        assert_eq!(correct_crlf(&text, 7), 7);
850        assert_eq!(correct_crlf(&text, text.len()), text.len());
851    }
852
853    #[test]
854    fn test_end_of_line() {
855        let ed = make_ed("abc\ndef\nghi");
856        let mut aff = CursorAffinity::Backward;
857        assert_eq!(end_of_line(&ed, &mut aff, 0, Mode::Insert).0, 3);
858        assert_eq!(aff, CursorAffinity::Backward);
859        assert_eq!(end_of_line(&ed, &mut aff, 1, Mode::Insert).0, 3);
860        assert_eq!(aff, CursorAffinity::Backward);
861        assert_eq!(end_of_line(&ed, &mut aff, 3, Mode::Insert).0, 3);
862        assert_eq!(aff, CursorAffinity::Backward);
863
864        assert_eq!(end_of_line(&ed, &mut aff, 4, Mode::Insert).0, 7);
865        assert_eq!(end_of_line(&ed, &mut aff, 5, Mode::Insert).0, 7);
866        assert_eq!(end_of_line(&ed, &mut aff, 7, Mode::Insert).0, 7);
867
868        let ed = make_ed("abc\r\ndef\r\nghi");
869        let mut aff = CursorAffinity::Forward;
870        assert_eq!(end_of_line(&ed, &mut aff, 0, Mode::Insert).0, 3);
871        assert_eq!(aff, CursorAffinity::Backward);
872
873        assert_eq!(end_of_line(&ed, &mut aff, 1, Mode::Insert).0, 3);
874        assert_eq!(aff, CursorAffinity::Backward);
875        assert_eq!(end_of_line(&ed, &mut aff, 3, Mode::Insert).0, 3);
876        assert_eq!(aff, CursorAffinity::Backward);
877
878        assert_eq!(end_of_line(&ed, &mut aff, 5, Mode::Insert).0, 8);
879        assert_eq!(end_of_line(&ed, &mut aff, 6, Mode::Insert).0, 8);
880        assert_eq!(end_of_line(&ed, &mut aff, 7, Mode::Insert).0, 8);
881        assert_eq!(end_of_line(&ed, &mut aff, 8, Mode::Insert).0, 8);
882
883        let ed = make_ed("testing\r\nAbout\r\nblah");
884        let mut aff = CursorAffinity::Backward;
885        assert_eq!(end_of_line(&ed, &mut aff, 0, Mode::Insert).0, 7);
886    }
887
888    #[test]
889    fn test_move_down() {
890        let ed = make_ed("abc\n\n\ndef\n\nghi");
891
892        let mut aff = CursorAffinity::Forward;
893
894        assert_eq!(move_down(&ed, 0, &mut aff, None, Mode::Insert, 1).0, 4);
895
896        let (offset, horiz) = move_down(&ed, 1, &mut aff, None, Mode::Insert, 1);
897        assert_eq!(offset, 4);
898        assert!(matches!(horiz, ColPosition::Col(_)));
899        let (offset, horiz) = move_down(&ed, 4, &mut aff, Some(horiz), Mode::Insert, 1);
900        assert_eq!(offset, 5);
901        assert!(matches!(horiz, ColPosition::Col(_)));
902        let (offset, _) = move_down(&ed, 5, &mut aff, Some(horiz), Mode::Insert, 1);
903        // Moving down with a horiz starting from position 1 on first line will put cursor at
904        // (approximately) position 1 on the next line with content they arrive at
905        assert_eq!(offset, 7);
906    }
907
908    #[test]
909    fn test_move_up() {
910        let ed = make_ed("abc\n\n\ndef\n\nghi");
911
912        let mut aff = CursorAffinity::Forward;
913
914        assert_eq!(move_up(&ed, 0, &mut aff, None, Mode::Insert, 1).0, 0);
915
916        let (offset, horiz) = move_up(&ed, 7, &mut aff, None, Mode::Insert, 1);
917        assert_eq!(offset, 5);
918        assert!(matches!(horiz, ColPosition::Col(_)));
919        let (offset, horiz) = move_up(&ed, 5, &mut aff, Some(horiz), Mode::Insert, 1);
920        assert_eq!(offset, 4);
921        assert!(matches!(horiz, ColPosition::Col(_)));
922        let (offset, _) = move_up(&ed, 4, &mut aff, Some(horiz), Mode::Insert, 1);
923        // Moving up with a horiz starting from position 1 on first line will put cursor at
924        // (approximately) position 1 on the next line with content they arrive at
925        assert_eq!(offset, 1);
926    }
927}