normal.rs

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