normal.rs

  1mod change;
  2mod delete;
  3mod yank;
  4
  5use std::{borrow::Cow, cmp::Ordering};
  6
  7use crate::{
  8    motion::Motion,
  9    object::Object,
 10    state::{Mode, Operator},
 11    Vim,
 12};
 13use collections::{HashMap, HashSet};
 14use editor::{
 15    display_map::ToDisplayPoint,
 16    scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
 17    Anchor, Bias, ClipboardSelection, DisplayPoint, Editor,
 18};
 19use gpui::{actions, impl_actions, MutableAppContext, ViewContext};
 20use language::{AutoindentMode, Point, SelectionGoal};
 21use log::error;
 22use serde::Deserialize;
 23use workspace::Workspace;
 24
 25use self::{
 26    change::{change_motion, change_object},
 27    delete::{delete_motion, delete_object},
 28    yank::{yank_motion, yank_object},
 29};
 30
 31#[derive(Clone, PartialEq, Deserialize)]
 32struct Scroll(ScrollAmount);
 33
 34actions!(
 35    vim,
 36    [
 37        InsertAfter,
 38        InsertFirstNonWhitespace,
 39        InsertEndOfLine,
 40        InsertLineAbove,
 41        InsertLineBelow,
 42        DeleteLeft,
 43        DeleteRight,
 44        ChangeToEndOfLine,
 45        DeleteToEndOfLine,
 46        Paste,
 47        Yank,
 48    ]
 49);
 50
 51impl_actions!(vim, [Scroll]);
 52
 53pub fn init(cx: &mut MutableAppContext) {
 54    cx.add_action(insert_after);
 55    cx.add_action(insert_first_non_whitespace);
 56    cx.add_action(insert_end_of_line);
 57    cx.add_action(insert_line_above);
 58    cx.add_action(insert_line_below);
 59    cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
 60        Vim::update(cx, |vim, cx| {
 61            let times = vim.pop_number_operator(cx);
 62            delete_motion(vim, Motion::Left, times, cx);
 63        })
 64    });
 65    cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
 66        Vim::update(cx, |vim, cx| {
 67            let times = vim.pop_number_operator(cx);
 68            delete_motion(vim, Motion::Right, times, cx);
 69        })
 70    });
 71    cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
 72        Vim::update(cx, |vim, cx| {
 73            let times = vim.pop_number_operator(cx);
 74            change_motion(vim, Motion::EndOfLine, times, cx);
 75        })
 76    });
 77    cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
 78        Vim::update(cx, |vim, cx| {
 79            let times = vim.pop_number_operator(cx);
 80            delete_motion(vim, Motion::EndOfLine, times, cx);
 81        })
 82    });
 83    cx.add_action(paste);
 84    cx.add_action(|_: &mut Workspace, Scroll(amount): &Scroll, cx| {
 85        Vim::update(cx, |vim, cx| {
 86            vim.update_active_editor(cx, |editor, cx| {
 87                scroll(editor, amount, cx);
 88            })
 89        })
 90    });
 91}
 92
 93pub fn normal_motion(
 94    motion: Motion,
 95    operator: Option<Operator>,
 96    times: usize,
 97    cx: &mut MutableAppContext,
 98) {
 99    Vim::update(cx, |vim, cx| {
100        match operator {
101            None => move_cursor(vim, motion, times, cx),
102            Some(Operator::Change) => change_motion(vim, motion, times, cx),
103            Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
104            Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
105            Some(operator) => {
106                // Can't do anything for text objects or namespace operators. Ignoring
107                error!("Unexpected normal mode motion operator: {:?}", operator)
108            }
109        }
110    });
111}
112
113pub fn normal_object(object: Object, cx: &mut MutableAppContext) {
114    Vim::update(cx, |vim, cx| {
115        match vim.state.operator_stack.pop() {
116            Some(Operator::Object { around }) => match vim.state.operator_stack.pop() {
117                Some(Operator::Change) => change_object(vim, object, around, cx),
118                Some(Operator::Delete) => delete_object(vim, object, around, cx),
119                Some(Operator::Yank) => yank_object(vim, object, around, cx),
120                _ => {
121                    // Can't do anything for namespace operators. Ignoring
122                }
123            },
124            _ => {
125                // Can't do anything with change/delete/yank and text objects. Ignoring
126            }
127        }
128        vim.clear_operator(cx);
129    })
130}
131
132fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
133    vim.update_active_editor(cx, |editor, cx| {
134        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
135            s.move_cursors_with(|map, cursor, goal| {
136                motion
137                    .move_point(map, cursor, goal, times)
138                    .unwrap_or((cursor, goal))
139            })
140        })
141    });
142}
143
144fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
145    Vim::update(cx, |vim, cx| {
146        vim.switch_mode(Mode::Insert, false, cx);
147        vim.update_active_editor(cx, |editor, cx| {
148            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
149                s.maybe_move_cursors_with(|map, cursor, goal| {
150                    Motion::Right.move_point(map, cursor, goal, 1)
151                });
152            });
153        });
154    });
155}
156
157fn insert_first_non_whitespace(
158    _: &mut Workspace,
159    _: &InsertFirstNonWhitespace,
160    cx: &mut ViewContext<Workspace>,
161) {
162    Vim::update(cx, |vim, cx| {
163        vim.switch_mode(Mode::Insert, false, cx);
164        vim.update_active_editor(cx, |editor, cx| {
165            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
166                s.maybe_move_cursors_with(|map, cursor, goal| {
167                    Motion::FirstNonWhitespace.move_point(map, cursor, goal, 1)
168                });
169            });
170        });
171    });
172}
173
174fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
175    Vim::update(cx, |vim, cx| {
176        vim.switch_mode(Mode::Insert, false, cx);
177        vim.update_active_editor(cx, |editor, cx| {
178            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
179                s.maybe_move_cursors_with(|map, cursor, goal| {
180                    Motion::EndOfLine.move_point(map, cursor, goal, 1)
181                });
182            });
183        });
184    });
185}
186
187fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
188    Vim::update(cx, |vim, cx| {
189        vim.switch_mode(Mode::Insert, false, cx);
190        vim.update_active_editor(cx, |editor, cx| {
191            editor.transact(cx, |editor, cx| {
192                let (map, old_selections) = editor.selections.all_display(cx);
193                let selection_start_rows: HashSet<u32> = old_selections
194                    .into_iter()
195                    .map(|selection| selection.start.row())
196                    .collect();
197                let edits = selection_start_rows.into_iter().map(|row| {
198                    let (indent, _) = map.line_indent(row);
199                    let start_of_line = map
200                        .clip_point(DisplayPoint::new(row, 0), Bias::Left)
201                        .to_point(&map);
202                    let mut new_text = " ".repeat(indent as usize);
203                    new_text.push('\n');
204                    (start_of_line..start_of_line, new_text)
205                });
206                editor.edit_with_autoindent(edits, cx);
207                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
208                    s.move_cursors_with(|map, mut cursor, _| {
209                        *cursor.row_mut() -= 1;
210                        *cursor.column_mut() = map.line_len(cursor.row());
211                        (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
212                    });
213                });
214            });
215        });
216    });
217}
218
219fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
220    Vim::update(cx, |vim, cx| {
221        vim.switch_mode(Mode::Insert, false, cx);
222        vim.update_active_editor(cx, |editor, cx| {
223            editor.transact(cx, |editor, cx| {
224                let (map, old_selections) = editor.selections.all_display(cx);
225                let selection_end_rows: HashSet<u32> = old_selections
226                    .into_iter()
227                    .map(|selection| selection.end.row())
228                    .collect();
229                let edits = selection_end_rows.into_iter().map(|row| {
230                    let (indent, _) = map.line_indent(row);
231                    let end_of_line = map
232                        .clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left)
233                        .to_point(&map);
234                    let mut new_text = "\n".to_string();
235                    new_text.push_str(&" ".repeat(indent as usize));
236                    (end_of_line..end_of_line, new_text)
237                });
238                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
239                    s.maybe_move_cursors_with(|map, cursor, goal| {
240                        Motion::EndOfLine.move_point(map, cursor, goal, 1)
241                    });
242                });
243                editor.edit_with_autoindent(edits, cx);
244            });
245        });
246    });
247}
248
249fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
250    Vim::update(cx, |vim, cx| {
251        vim.update_active_editor(cx, |editor, cx| {
252            editor.transact(cx, |editor, cx| {
253                editor.set_clip_at_line_ends(false, cx);
254                if let Some(item) = cx.as_mut().read_from_clipboard() {
255                    let mut clipboard_text = Cow::Borrowed(item.text());
256                    if let Some(mut clipboard_selections) =
257                        item.metadata::<Vec<ClipboardSelection>>()
258                    {
259                        let (display_map, selections) = editor.selections.all_display(cx);
260                        let all_selections_were_entire_line =
261                            clipboard_selections.iter().all(|s| s.is_entire_line);
262                        if clipboard_selections.len() != selections.len() {
263                            let mut newline_separated_text = String::new();
264                            let mut clipboard_selections =
265                                clipboard_selections.drain(..).peekable();
266                            let mut ix = 0;
267                            while let Some(clipboard_selection) = clipboard_selections.next() {
268                                newline_separated_text
269                                    .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
270                                ix += clipboard_selection.len;
271                                if clipboard_selections.peek().is_some() {
272                                    newline_separated_text.push('\n');
273                                }
274                            }
275                            clipboard_text = Cow::Owned(newline_separated_text);
276                        }
277
278                        // If the pasted text is a single line, the cursor should be placed after
279                        // the newly pasted text. This is easiest done with an anchor after the
280                        // insertion, and then with a fixup to move the selection back one position.
281                        // However if the pasted text is linewise, the cursor should be placed at the start
282                        // of the new text on the following line. This is easiest done with a manually adjusted
283                        // point.
284                        // This enum lets us represent both cases
285                        enum NewPosition {
286                            Inside(Point),
287                            After(Anchor),
288                        }
289                        let mut new_selections: HashMap<usize, NewPosition> = Default::default();
290                        editor.buffer().update(cx, |buffer, cx| {
291                            let snapshot = buffer.snapshot(cx);
292                            let mut start_offset = 0;
293                            let mut edits = Vec::new();
294                            for (ix, selection) in selections.iter().enumerate() {
295                                let to_insert;
296                                let linewise;
297                                if let Some(clipboard_selection) = clipboard_selections.get(ix) {
298                                    let end_offset = start_offset + clipboard_selection.len;
299                                    to_insert = &clipboard_text[start_offset..end_offset];
300                                    linewise = clipboard_selection.is_entire_line;
301                                    start_offset = end_offset;
302                                } else {
303                                    to_insert = clipboard_text.as_str();
304                                    linewise = all_selections_were_entire_line;
305                                }
306
307                                // If the clipboard text was copied linewise, and the current selection
308                                // is empty, then paste the text after this line and move the selection
309                                // to the start of the pasted text
310                                let insert_at = if linewise {
311                                    let (point, _) = display_map
312                                        .next_line_boundary(selection.start.to_point(&display_map));
313
314                                    if !to_insert.starts_with('\n') {
315                                        // Add newline before pasted text so that it shows up
316                                        edits.push((point..point, "\n"));
317                                    }
318                                    // Drop selection at the start of the next line
319                                    new_selections.insert(
320                                        selection.id,
321                                        NewPosition::Inside(Point::new(point.row + 1, 0)),
322                                    );
323                                    point
324                                } else {
325                                    let mut point = selection.end;
326                                    // Paste the text after the current selection
327                                    *point.column_mut() = point.column() + 1;
328                                    let point = display_map
329                                        .clip_point(point, Bias::Right)
330                                        .to_point(&display_map);
331
332                                    new_selections.insert(
333                                        selection.id,
334                                        if to_insert.contains('\n') {
335                                            NewPosition::Inside(point)
336                                        } else {
337                                            NewPosition::After(snapshot.anchor_after(point))
338                                        },
339                                    );
340                                    point
341                                };
342
343                                if linewise && to_insert.ends_with('\n') {
344                                    edits.push((
345                                        insert_at..insert_at,
346                                        &to_insert[0..to_insert.len().saturating_sub(1)],
347                                    ))
348                                } else {
349                                    edits.push((insert_at..insert_at, to_insert));
350                                }
351                            }
352                            drop(snapshot);
353                            buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
354                        });
355
356                        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
357                            s.move_with(|map, selection| {
358                                if let Some(new_position) = new_selections.get(&selection.id) {
359                                    match new_position {
360                                        NewPosition::Inside(new_point) => {
361                                            selection.collapse_to(
362                                                new_point.to_display_point(map),
363                                                SelectionGoal::None,
364                                            );
365                                        }
366                                        NewPosition::After(after_point) => {
367                                            let mut new_point = after_point.to_display_point(map);
368                                            *new_point.column_mut() =
369                                                new_point.column().saturating_sub(1);
370                                            new_point = map.clip_point(new_point, Bias::Left);
371                                            selection.collapse_to(new_point, SelectionGoal::None);
372                                        }
373                                    }
374                                }
375                            });
376                        });
377                    } else {
378                        editor.insert(&clipboard_text, cx);
379                    }
380                }
381                editor.set_clip_at_line_ends(true, cx);
382            });
383        });
384    });
385}
386
387fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
388    let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
389    editor.scroll_screen(amount, cx);
390    if should_move_cursor {
391        let selection_ordering = editor.newest_selection_on_screen(cx);
392        if selection_ordering.is_eq() {
393            return;
394        }
395
396        let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
397            visible_rows as u32
398        } else {
399            return;
400        };
401
402        let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
403        let top_anchor = editor.scroll_manager.anchor().top_anchor;
404
405        editor.change_selections(None, cx, |s| {
406            s.replace_cursors_with(|snapshot| {
407                let mut new_point = top_anchor.to_display_point(&snapshot);
408
409                match selection_ordering {
410                    Ordering::Less => {
411                        *new_point.row_mut() += scroll_margin_rows;
412                        new_point = snapshot.clip_point(new_point, Bias::Right);
413                    }
414                    Ordering::Greater => {
415                        *new_point.row_mut() += visible_rows - scroll_margin_rows as u32;
416                        new_point = snapshot.clip_point(new_point, Bias::Left);
417                    }
418                    Ordering::Equal => unreachable!(),
419                }
420
421                vec![new_point]
422            })
423        });
424    }
425}
426
427#[cfg(test)]
428mod test {
429    use indoc::indoc;
430
431    use crate::{
432        state::{
433            Mode::{self, *},
434            Namespace, Operator,
435        },
436        test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
437    };
438
439    #[gpui::test]
440    async fn test_h(cx: &mut gpui::TestAppContext) {
441        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
442        cx.assert_all(indoc! {"
443            ˇThe qˇuick
444            ˇbrown"
445        })
446        .await;
447    }
448
449    #[gpui::test]
450    async fn test_backspace(cx: &mut gpui::TestAppContext) {
451        let mut cx = NeovimBackedTestContext::new(cx)
452            .await
453            .binding(["backspace"]);
454        cx.assert_all(indoc! {"
455            ˇThe qˇuick
456            ˇbrown"
457        })
458        .await;
459    }
460
461    #[gpui::test]
462    async fn test_j(cx: &mut gpui::TestAppContext) {
463        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
464        cx.assert_all(indoc! {"
465            ˇThe qˇuick broˇwn
466            ˇfox jumps"
467        })
468        .await;
469    }
470
471    #[gpui::test]
472    async fn test_k(cx: &mut gpui::TestAppContext) {
473        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
474        cx.assert_all(indoc! {"
475            ˇThe qˇuick
476            ˇbrown fˇox jumˇps"
477        })
478        .await;
479    }
480
481    #[gpui::test]
482    async fn test_l(cx: &mut gpui::TestAppContext) {
483        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
484        cx.assert_all(indoc! {"
485            ˇThe qˇuicˇk
486            ˇbrowˇn"})
487            .await;
488    }
489
490    #[gpui::test]
491    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
492        let mut cx = NeovimBackedTestContext::new(cx).await;
493        cx.assert_binding_matches_all(
494            ["$"],
495            indoc! {"
496            ˇThe qˇuicˇk
497            ˇbrowˇn"},
498        )
499        .await;
500        cx.assert_binding_matches_all(
501            ["0"],
502            indoc! {"
503                ˇThe qˇuicˇk
504                ˇbrowˇn"},
505        )
506        .await;
507    }
508
509    #[gpui::test]
510    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
511        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
512
513        cx.assert_all(indoc! {"
514                The ˇquick
515                
516                brown fox jumps
517                overˇ the lazy doˇg"})
518            .await;
519        cx.assert(indoc! {"
520            The quiˇck
521            
522            brown"})
523            .await;
524        cx.assert(indoc! {"
525            The quiˇck
526            
527            "})
528            .await;
529    }
530
531    #[gpui::test]
532    async fn test_w(cx: &mut gpui::TestAppContext) {
533        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
534        cx.assert_all(indoc! {"
535            The ˇquickˇ-ˇbrown
536            ˇ
537            ˇ
538            ˇfox_jumps ˇover
539            ˇthˇe"})
540            .await;
541        let mut cx = cx.binding(["shift-w"]);
542        cx.assert_all(indoc! {"
543            The ˇquickˇ-ˇbrown
544            ˇ
545            ˇ
546            ˇfox_jumps ˇover
547            ˇthˇe"})
548            .await;
549    }
550
551    #[gpui::test]
552    async fn test_e(cx: &mut gpui::TestAppContext) {
553        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
554        cx.assert_all(indoc! {"
555            Thˇe quicˇkˇ-browˇn
556            
557            
558            fox_jumpˇs oveˇr
559            thˇe"})
560            .await;
561        let mut cx = cx.binding(["shift-e"]);
562        cx.assert_all(indoc! {"
563            Thˇe quicˇkˇ-browˇn
564            
565            
566            fox_jumpˇs oveˇr
567            thˇe"})
568            .await;
569    }
570
571    #[gpui::test]
572    async fn test_b(cx: &mut gpui::TestAppContext) {
573        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
574        cx.assert_all(indoc! {"
575            ˇThe ˇquickˇ-ˇbrown
576            ˇ
577            ˇ
578            ˇfox_jumps ˇover
579            ˇthe"})
580            .await;
581        let mut cx = cx.binding(["shift-b"]);
582        cx.assert_all(indoc! {"
583            ˇThe ˇquickˇ-ˇbrown
584            ˇ
585            ˇ
586            ˇfox_jumps ˇover
587            ˇthe"})
588            .await;
589    }
590
591    #[gpui::test]
592    async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
593        let mut cx = VimTestContext::new(cx, true).await;
594
595        // Can abort with escape to get back to normal mode
596        cx.simulate_keystroke("g");
597        assert_eq!(cx.mode(), Normal);
598        assert_eq!(
599            cx.active_operator(),
600            Some(Operator::Namespace(Namespace::G))
601        );
602        cx.simulate_keystroke("escape");
603        assert_eq!(cx.mode(), Normal);
604        assert_eq!(cx.active_operator(), None);
605    }
606
607    #[gpui::test]
608    async fn test_gg(cx: &mut gpui::TestAppContext) {
609        let mut cx = NeovimBackedTestContext::new(cx).await;
610        cx.assert_binding_matches_all(
611            ["g", "g"],
612            indoc! {"
613                The qˇuick
614            
615                brown fox jumps
616                over ˇthe laˇzy dog"},
617        )
618        .await;
619        cx.assert_binding_matches(
620            ["g", "g"],
621            indoc! {"
622                
623            
624                brown fox jumps
625                over the laˇzy dog"},
626        )
627        .await;
628        cx.assert_binding_matches(
629            ["2", "g", "g"],
630            indoc! {"
631                ˇ
632                
633                brown fox jumps
634                over the lazydog"},
635        )
636        .await;
637    }
638
639    #[gpui::test]
640    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
641        let mut cx = NeovimBackedTestContext::new(cx).await;
642        cx.assert_binding_matches_all(
643            ["shift-g"],
644            indoc! {"
645                The qˇuick
646                
647                brown fox jumps
648                over ˇthe laˇzy dog"},
649        )
650        .await;
651        cx.assert_binding_matches(
652            ["shift-g"],
653            indoc! {"
654                
655                
656                brown fox jumps
657                over the laˇzy dog"},
658        )
659        .await;
660        cx.assert_binding_matches(
661            ["2", "shift-g"],
662            indoc! {"
663                ˇ
664                
665                brown fox jumps
666                over the lazydog"},
667        )
668        .await;
669    }
670
671    #[gpui::test]
672    async fn test_a(cx: &mut gpui::TestAppContext) {
673        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
674        cx.assert_all("The qˇuicˇk").await;
675    }
676
677    #[gpui::test]
678    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
679        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
680        cx.assert_all(indoc! {"
681            ˇ
682            The qˇuick
683            brown ˇfox "})
684            .await;
685    }
686
687    #[gpui::test]
688    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
689        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
690        cx.assert("The qˇuick").await;
691        cx.assert(" The qˇuick").await;
692        cx.assert("ˇ").await;
693        cx.assert(indoc! {"
694                The qˇuick
695                brown fox"})
696            .await;
697        cx.assert(indoc! {"
698                ˇ
699                The quick"})
700            .await;
701        // Indoc disallows trailing whitspace.
702        cx.assert("   ˇ \nThe quick").await;
703    }
704
705    #[gpui::test]
706    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
707        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
708        cx.assert("The qˇuick").await;
709        cx.assert(" The qˇuick").await;
710        cx.assert("ˇ").await;
711        cx.assert(indoc! {"
712                The qˇuick
713                brown fox"})
714            .await;
715        cx.assert(indoc! {"
716                ˇ
717                The quick"})
718            .await;
719    }
720
721    #[gpui::test]
722    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
723        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
724        cx.assert(indoc! {"
725                The qˇuick
726                brown fox"})
727            .await;
728        cx.assert(indoc! {"
729                The quick
730                ˇ
731                brown fox"})
732            .await;
733    }
734
735    #[gpui::test]
736    async fn test_x(cx: &mut gpui::TestAppContext) {
737        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
738        cx.assert_all("ˇTeˇsˇt").await;
739        cx.assert(indoc! {"
740                Tesˇt
741                test"})
742            .await;
743    }
744
745    #[gpui::test]
746    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
747        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
748        cx.assert_all("ˇTˇeˇsˇt").await;
749        cx.assert(indoc! {"
750                Test
751                ˇtest"})
752            .await;
753    }
754
755    #[gpui::test]
756    async fn test_o(cx: &mut gpui::TestAppContext) {
757        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
758        cx.assert("ˇ").await;
759        cx.assert("The ˇquick").await;
760        cx.assert_all(indoc! {"
761                The qˇuick
762                brown ˇfox
763                jumps ˇover"})
764            .await;
765        cx.assert(indoc! {"
766                The quick
767                ˇ
768                brown fox"})
769            .await;
770        cx.assert(indoc! {"
771                fn test() {
772                    println!(ˇ);
773                }
774            "})
775            .await;
776        cx.assert(indoc! {"
777                fn test(ˇ) {
778                    println!();
779                }"})
780            .await;
781    }
782
783    #[gpui::test]
784    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
785        let cx = NeovimBackedTestContext::new(cx).await;
786        let mut cx = cx.binding(["shift-o"]);
787        cx.assert("ˇ").await;
788        cx.assert("The ˇquick").await;
789        cx.assert_all(indoc! {"
790            The qˇuick
791            brown ˇfox
792            jumps ˇover"})
793            .await;
794        cx.assert(indoc! {"
795            The quick
796            ˇ
797            brown fox"})
798            .await;
799
800        // Our indentation is smarter than vims. So we don't match here
801        cx.assert_manual(
802            indoc! {"
803                fn test()
804                    println!(ˇ);"},
805            Mode::Normal,
806            indoc! {"
807                fn test()
808                    ˇ
809                    println!();"},
810            Mode::Insert,
811        );
812        cx.assert_manual(
813            indoc! {"
814                fn test(ˇ) {
815                    println!();
816                }"},
817            Mode::Normal,
818            indoc! {"
819                ˇ
820                fn test() {
821                    println!();
822                }"},
823            Mode::Insert,
824        );
825    }
826
827    #[gpui::test]
828    async fn test_dd(cx: &mut gpui::TestAppContext) {
829        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
830        cx.assert("ˇ").await;
831        cx.assert("The ˇquick").await;
832        cx.assert_all(indoc! {"
833                The qˇuick
834                brown ˇfox
835                jumps ˇover"})
836            .await;
837        cx.assert_exempted(
838            indoc! {"
839                The quick
840                ˇ
841                brown fox"},
842            ExemptionFeatures::DeletionOnEmptyLine,
843        )
844        .await;
845    }
846
847    #[gpui::test]
848    async fn test_cc(cx: &mut gpui::TestAppContext) {
849        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
850        cx.assert("ˇ").await;
851        cx.assert("The ˇquick").await;
852        cx.assert_all(indoc! {"
853                The quˇick
854                brown ˇfox
855                jumps ˇover"})
856            .await;
857        cx.assert(indoc! {"
858                The quick
859                ˇ
860                brown fox"})
861            .await;
862    }
863
864    #[gpui::test]
865    async fn test_p(cx: &mut gpui::TestAppContext) {
866        let mut cx = NeovimBackedTestContext::new(cx).await;
867        cx.set_shared_state(indoc! {"
868                The quick brown
869                fox juˇmps over
870                the lazy dog"})
871            .await;
872
873        cx.simulate_shared_keystrokes(["d", "d"]).await;
874        cx.assert_state_matches().await;
875
876        cx.simulate_shared_keystroke("p").await;
877        cx.assert_state_matches().await;
878
879        cx.set_shared_state(indoc! {"
880                The quick brown
881                fox ˇjumps over
882                the lazy dog"})
883            .await;
884        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
885        cx.set_shared_state(indoc! {"
886                The quick brown
887                fox jumps oveˇr
888                the lazy dog"})
889            .await;
890        cx.simulate_shared_keystroke("p").await;
891        cx.assert_state_matches().await;
892    }
893
894    #[gpui::test]
895    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
896        let mut cx = NeovimBackedTestContext::new(cx).await;
897
898        for count in 1..=5 {
899            cx.assert_binding_matches_all(
900                [&count.to_string(), "w"],
901                indoc! {"
902                    ˇThe quˇickˇ browˇn
903                    ˇ
904                    ˇfox ˇjumpsˇ-ˇoˇver
905                    ˇthe lazy dog
906                "},
907            )
908            .await;
909        }
910    }
911
912    #[gpui::test]
913    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
914        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
915        cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
916    }
917
918    #[gpui::test]
919    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
920        let mut cx = NeovimBackedTestContext::new(cx).await;
921        for count in 1..=3 {
922            let test_case = indoc! {"
923                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
924                ˇ    ˇbˇaaˇa ˇbˇbˇb
925                ˇ   
926                ˇb
927            "};
928
929            cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
930                .await;
931
932            cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
933                .await;
934        }
935    }
936
937    #[gpui::test]
938    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
939        let mut cx = NeovimBackedTestContext::new(cx).await;
940        for count in 1..=3 {
941            let test_case = indoc! {"
942                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
943                ˇ    ˇbˇaaˇa ˇbˇbˇb
944                ˇ   
945                ˇb
946            "};
947
948            cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
949                .await;
950
951            cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
952                .await;
953        }
954    }
955}