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