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
427pub(crate) fn normal_replace(text: &str, cx: &mut MutableAppContext) {
428    Vim::update(cx, |vim, cx| {
429        vim.update_active_editor(cx, |editor, cx| {
430            editor.transact(cx, |editor, cx| {
431                editor.set_clip_at_line_ends(false, cx);
432                editor.change_selections(None, cx, |s| {
433                    s.move_with(|map, selection| {
434                        *selection.end.column_mut() += 1;
435                        selection.end = map.clip_point(selection.end, Bias::Right);
436                    });
437                });
438                editor.insert(text, cx);
439                editor.set_clip_at_line_ends(true, cx);
440            });
441        });
442        vim.pop_operator(cx)
443    });
444}
445
446#[cfg(test)]
447mod test {
448    use indoc::indoc;
449
450    use crate::{
451        state::{
452            Mode::{self, *},
453            Namespace, Operator,
454        },
455        test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
456    };
457
458    #[gpui::test]
459    async fn test_h(cx: &mut gpui::TestAppContext) {
460        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
461        cx.assert_all(indoc! {"
462            ˇThe qˇuick
463            ˇbrown"
464        })
465        .await;
466    }
467
468    #[gpui::test]
469    async fn test_backspace(cx: &mut gpui::TestAppContext) {
470        let mut cx = NeovimBackedTestContext::new(cx)
471            .await
472            .binding(["backspace"]);
473        cx.assert_all(indoc! {"
474            ˇThe qˇuick
475            ˇbrown"
476        })
477        .await;
478    }
479
480    #[gpui::test]
481    async fn test_j(cx: &mut gpui::TestAppContext) {
482        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
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_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
612        let mut cx = VimTestContext::new(cx, true).await;
613
614        // Can abort with escape to get back to normal mode
615        cx.simulate_keystroke("g");
616        assert_eq!(cx.mode(), Normal);
617        assert_eq!(
618            cx.active_operator(),
619            Some(Operator::Namespace(Namespace::G))
620        );
621        cx.simulate_keystroke("escape");
622        assert_eq!(cx.mode(), Normal);
623        assert_eq!(cx.active_operator(), None);
624    }
625
626    #[gpui::test]
627    async fn test_gg(cx: &mut gpui::TestAppContext) {
628        let mut cx = NeovimBackedTestContext::new(cx).await;
629        cx.assert_binding_matches_all(
630            ["g", "g"],
631            indoc! {"
632                The qˇuick
633            
634                brown fox jumps
635                over ˇthe laˇzy dog"},
636        )
637        .await;
638        cx.assert_binding_matches(
639            ["g", "g"],
640            indoc! {"
641                
642            
643                brown fox jumps
644                over the laˇzy dog"},
645        )
646        .await;
647        cx.assert_binding_matches(
648            ["2", "g", "g"],
649            indoc! {"
650                ˇ
651                
652                brown fox jumps
653                over the lazydog"},
654        )
655        .await;
656    }
657
658    #[gpui::test]
659    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
660        let mut cx = NeovimBackedTestContext::new(cx).await;
661        cx.assert_binding_matches_all(
662            ["shift-g"],
663            indoc! {"
664                The qˇuick
665                
666                brown fox jumps
667                over ˇthe laˇzy dog"},
668        )
669        .await;
670        cx.assert_binding_matches(
671            ["shift-g"],
672            indoc! {"
673                
674                
675                brown fox jumps
676                over the laˇzy dog"},
677        )
678        .await;
679        cx.assert_binding_matches(
680            ["2", "shift-g"],
681            indoc! {"
682                ˇ
683                
684                brown fox jumps
685                over the lazydog"},
686        )
687        .await;
688    }
689
690    #[gpui::test]
691    async fn test_a(cx: &mut gpui::TestAppContext) {
692        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
693        cx.assert_all("The qˇuicˇk").await;
694    }
695
696    #[gpui::test]
697    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
698        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
699        cx.assert_all(indoc! {"
700            ˇ
701            The qˇuick
702            brown ˇfox "})
703            .await;
704    }
705
706    #[gpui::test]
707    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
708        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
709        cx.assert("The qˇuick").await;
710        cx.assert(" The qˇuick").await;
711        cx.assert("ˇ").await;
712        cx.assert(indoc! {"
713                The qˇuick
714                brown fox"})
715            .await;
716        cx.assert(indoc! {"
717                ˇ
718                The quick"})
719            .await;
720        // Indoc disallows trailing whitspace.
721        cx.assert("   ˇ \nThe quick").await;
722    }
723
724    #[gpui::test]
725    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
726        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
727        cx.assert("The qˇuick").await;
728        cx.assert(" The qˇuick").await;
729        cx.assert("ˇ").await;
730        cx.assert(indoc! {"
731                The qˇuick
732                brown fox"})
733            .await;
734        cx.assert(indoc! {"
735                ˇ
736                The quick"})
737            .await;
738    }
739
740    #[gpui::test]
741    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
742        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
743        cx.assert(indoc! {"
744                The qˇuick
745                brown fox"})
746            .await;
747        cx.assert(indoc! {"
748                The quick
749                ˇ
750                brown fox"})
751            .await;
752    }
753
754    #[gpui::test]
755    async fn test_x(cx: &mut gpui::TestAppContext) {
756        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
757        cx.assert_all("ˇTeˇsˇt").await;
758        cx.assert(indoc! {"
759                Tesˇt
760                test"})
761            .await;
762    }
763
764    #[gpui::test]
765    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
766        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
767        cx.assert_all("ˇTˇeˇsˇt").await;
768        cx.assert(indoc! {"
769                Test
770                ˇtest"})
771            .await;
772    }
773
774    #[gpui::test]
775    async fn test_o(cx: &mut gpui::TestAppContext) {
776        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
777        cx.assert("ˇ").await;
778        cx.assert("The ˇquick").await;
779        cx.assert_all(indoc! {"
780                The qˇuick
781                brown ˇfox
782                jumps ˇover"})
783            .await;
784        cx.assert(indoc! {"
785                The quick
786                ˇ
787                brown fox"})
788            .await;
789        cx.assert(indoc! {"
790                fn test() {
791                    println!(ˇ);
792                }
793            "})
794            .await;
795        cx.assert(indoc! {"
796                fn test(ˇ) {
797                    println!();
798                }"})
799            .await;
800    }
801
802    #[gpui::test]
803    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
804        let cx = NeovimBackedTestContext::new(cx).await;
805        let mut cx = cx.binding(["shift-o"]);
806        cx.assert("ˇ").await;
807        cx.assert("The ˇquick").await;
808        cx.assert_all(indoc! {"
809            The qˇuick
810            brown ˇfox
811            jumps ˇover"})
812            .await;
813        cx.assert(indoc! {"
814            The quick
815            ˇ
816            brown fox"})
817            .await;
818
819        // Our indentation is smarter than vims. So we don't match here
820        cx.assert_manual(
821            indoc! {"
822                fn test()
823                    println!(ˇ);"},
824            Mode::Normal,
825            indoc! {"
826                fn test()
827                    ˇ
828                    println!();"},
829            Mode::Insert,
830        );
831        cx.assert_manual(
832            indoc! {"
833                fn test(ˇ) {
834                    println!();
835                }"},
836            Mode::Normal,
837            indoc! {"
838                ˇ
839                fn test() {
840                    println!();
841                }"},
842            Mode::Insert,
843        );
844    }
845
846    #[gpui::test]
847    async fn test_dd(cx: &mut gpui::TestAppContext) {
848        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
849        cx.assert("ˇ").await;
850        cx.assert("The ˇquick").await;
851        cx.assert_all(indoc! {"
852                The qˇuick
853                brown ˇfox
854                jumps ˇover"})
855            .await;
856        cx.assert_exempted(
857            indoc! {"
858                The quick
859                ˇ
860                brown fox"},
861            ExemptionFeatures::DeletionOnEmptyLine,
862        )
863        .await;
864    }
865
866    #[gpui::test]
867    async fn test_cc(cx: &mut gpui::TestAppContext) {
868        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
869        cx.assert("ˇ").await;
870        cx.assert("The ˇquick").await;
871        cx.assert_all(indoc! {"
872                The quˇick
873                brown ˇfox
874                jumps ˇover"})
875            .await;
876        cx.assert(indoc! {"
877                The quick
878                ˇ
879                brown fox"})
880            .await;
881    }
882
883    #[gpui::test]
884    async fn test_p(cx: &mut gpui::TestAppContext) {
885        let mut cx = NeovimBackedTestContext::new(cx).await;
886        cx.set_shared_state(indoc! {"
887                The quick brown
888                fox juˇmps over
889                the lazy dog"})
890            .await;
891
892        cx.simulate_shared_keystrokes(["d", "d"]).await;
893        cx.assert_state_matches().await;
894
895        cx.simulate_shared_keystroke("p").await;
896        cx.assert_state_matches().await;
897
898        cx.set_shared_state(indoc! {"
899                The quick brown
900                fox ˇjumps over
901                the lazy dog"})
902            .await;
903        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
904        cx.set_shared_state(indoc! {"
905                The quick brown
906                fox jumps oveˇr
907                the lazy dog"})
908            .await;
909        cx.simulate_shared_keystroke("p").await;
910        cx.assert_state_matches().await;
911    }
912
913    #[gpui::test]
914    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
915        let mut cx = NeovimBackedTestContext::new(cx).await;
916
917        for count in 1..=5 {
918            cx.assert_binding_matches_all(
919                [&count.to_string(), "w"],
920                indoc! {"
921                    ˇThe quˇickˇ browˇn
922                    ˇ
923                    ˇfox ˇjumpsˇ-ˇoˇver
924                    ˇthe lazy dog
925                "},
926            )
927            .await;
928        }
929    }
930
931    #[gpui::test]
932    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
933        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
934        cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
935    }
936
937    #[gpui::test]
938    async fn test_f_and_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(), "f", "b"], test_case)
949                .await;
950
951            cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
952                .await;
953        }
954    }
955
956    #[gpui::test]
957    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
958        let mut cx = NeovimBackedTestContext::new(cx).await;
959        for count in 1..=3 {
960            let test_case = indoc! {"
961                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
962                ˇ    ˇbˇaaˇa ˇbˇbˇb
963                ˇ   
964                ˇb
965            "};
966
967            cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
968                .await;
969
970            cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
971                .await;
972        }
973    }
974}