normal.rs

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