repeat.rs

   1use std::{cell::RefCell, rc::Rc};
   2
   3use crate::{
   4    Vim,
   5    insert::NormalBefore,
   6    motion::Motion,
   7    normal::InsertBefore,
   8    state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals},
   9};
  10use editor::Editor;
  11use gpui::{Action, App, Context, Window, actions};
  12use workspace::Workspace;
  13
  14actions!(
  15    vim,
  16    [
  17        /// Repeats the last change.
  18        Repeat,
  19        /// Ends the repeat recording.
  20        EndRepeat,
  21        /// Toggles macro recording.
  22        ToggleRecord,
  23        /// Replays the last recorded macro.
  24        ReplayLastRecording
  25    ]
  26);
  27
  28fn should_replay(action: &dyn Action) -> bool {
  29    // skip so that we don't leave the character palette open
  30    if editor::actions::ShowCharacterPalette.partial_eq(action) {
  31        return false;
  32    }
  33    true
  34}
  35
  36fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
  37    match action {
  38        ReplayableAction::Action(action) => {
  39            if super::InsertBefore.partial_eq(&**action)
  40                || super::InsertAfter.partial_eq(&**action)
  41                || super::InsertFirstNonWhitespace.partial_eq(&**action)
  42                || super::InsertEndOfLine.partial_eq(&**action)
  43            {
  44                Some(super::InsertBefore.boxed_clone())
  45            } else if super::InsertLineAbove.partial_eq(&**action)
  46                || super::InsertLineBelow.partial_eq(&**action)
  47            {
  48                Some(super::InsertLineBelow.boxed_clone())
  49            } else if crate::replace::ToggleReplace.partial_eq(&**action) {
  50                Some(crate::replace::ToggleReplace.boxed_clone())
  51            } else {
  52                None
  53            }
  54        }
  55        ReplayableAction::Insertion { .. } => None,
  56    }
  57}
  58
  59pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
  60    Vim::action(editor, cx, |vim, _: &EndRepeat, window, cx| {
  61        Vim::globals(cx).dot_replaying = false;
  62        vim.switch_mode(Mode::Normal, false, window, cx)
  63    });
  64
  65    Vim::action(editor, cx, |vim, _: &Repeat, window, cx| {
  66        vim.repeat(false, window, cx)
  67    });
  68
  69    Vim::action(editor, cx, |vim, _: &ToggleRecord, window, cx| {
  70        let globals = Vim::globals(cx);
  71        if let Some(char) = globals.recording_register.take() {
  72            globals.last_recorded_register = Some(char)
  73        } else {
  74            vim.push_operator(Operator::RecordRegister, window, cx);
  75        }
  76    });
  77
  78    Vim::action(editor, cx, |vim, _: &ReplayLastRecording, window, cx| {
  79        let Some(register) = Vim::globals(cx).last_recorded_register else {
  80            return;
  81        };
  82        vim.replay_register(register, window, cx)
  83    });
  84}
  85
  86pub struct ReplayerState {
  87    actions: Vec<ReplayableAction>,
  88    running: bool,
  89    ix: usize,
  90}
  91
  92#[derive(Clone)]
  93pub struct Replayer(Rc<RefCell<ReplayerState>>);
  94
  95impl Replayer {
  96    pub fn new() -> Self {
  97        Self(Rc::new(RefCell::new(ReplayerState {
  98            actions: vec![],
  99            running: false,
 100            ix: 0,
 101        })))
 102    }
 103
 104    pub fn replay(&mut self, actions: Vec<ReplayableAction>, window: &mut Window, cx: &mut App) {
 105        let mut lock = self.0.borrow_mut();
 106        let range = lock.ix..lock.ix;
 107        lock.actions.splice(range, actions);
 108        if lock.running {
 109            return;
 110        }
 111        lock.running = true;
 112        let this = self.clone();
 113        window.defer(cx, move |window, cx| {
 114            this.next(window, cx);
 115            let Some(workspace) = Workspace::for_window(window, cx) else {
 116                return;
 117            };
 118            let Some(editor) = workspace
 119                .read(cx)
 120                .active_item(cx)
 121                .and_then(|item| item.act_as::<Editor>(cx))
 122            else {
 123                return;
 124            };
 125            editor.update(cx, |editor, cx| {
 126                editor
 127                    .buffer()
 128                    .update(cx, |multi, cx| multi.finalize_last_transaction(cx))
 129            });
 130        })
 131    }
 132
 133    pub fn stop(self) {
 134        self.0.borrow_mut().actions.clear()
 135    }
 136
 137    pub fn next(self, window: &mut Window, cx: &mut App) {
 138        let mut lock = self.0.borrow_mut();
 139        let action = if lock.ix < 10000 {
 140            lock.actions.get(lock.ix).cloned()
 141        } else {
 142            log::error!("Aborting replay after 10000 actions");
 143            None
 144        };
 145        lock.ix += 1;
 146        drop(lock);
 147        let Some(action) = action else {
 148            // The `globals.dot_replaying = false` is a fail-safe to ensure that
 149            // this value is always reset, in the case that the focus is moved
 150            // away from the editor, effectively preventing the `EndRepeat`
 151            // action from being handled.
 152            let globals = Vim::globals(cx);
 153            globals.replayer.take();
 154            globals.dot_replaying = false;
 155            return;
 156        };
 157        match action {
 158            ReplayableAction::Action(action) => {
 159                if should_replay(&*action) {
 160                    window.dispatch_action(action.boxed_clone(), cx);
 161                    cx.defer(move |cx| Vim::globals(cx).observe_action(action.boxed_clone()));
 162                }
 163            }
 164            ReplayableAction::Insertion {
 165                text,
 166                utf16_range_to_replace,
 167            } => {
 168                let Some(workspace) = Workspace::for_window(window, cx) else {
 169                    return;
 170                };
 171                let Some(editor) = workspace
 172                    .read(cx)
 173                    .active_item(cx)
 174                    .and_then(|item| item.act_as::<Editor>(cx))
 175                else {
 176                    return;
 177                };
 178                editor.update(cx, |editor, cx| {
 179                    editor.replay_insert_event(&text, utf16_range_to_replace.clone(), window, cx)
 180                })
 181            }
 182        }
 183        window.defer(cx, move |window, cx| self.next(window, cx));
 184    }
 185}
 186
 187impl Vim {
 188    pub(crate) fn record_register(
 189        &mut self,
 190        register: char,
 191        window: &mut Window,
 192        cx: &mut Context<Self>,
 193    ) {
 194        let globals = Vim::globals(cx);
 195        globals.recording_register = Some(register);
 196        globals.recordings.remove(&register);
 197        globals.ignore_current_insertion = true;
 198        self.clear_operator(window, cx)
 199    }
 200
 201    pub(crate) fn replay_register(
 202        &mut self,
 203        mut register: char,
 204        window: &mut Window,
 205        cx: &mut Context<Self>,
 206    ) {
 207        let mut count = Vim::take_count(cx).unwrap_or(1);
 208        Vim::take_forced_motion(cx);
 209        self.clear_operator(window, cx);
 210
 211        let globals = Vim::globals(cx);
 212        if register == '@' {
 213            let Some(last) = globals.last_replayed_register else {
 214                return;
 215            };
 216            register = last;
 217        }
 218        let Some(actions) = globals.recordings.get(&register) else {
 219            return;
 220        };
 221
 222        let mut repeated_actions = vec![];
 223        while count > 0 {
 224            repeated_actions.extend(actions.iter().cloned());
 225            count -= 1
 226        }
 227
 228        globals.last_replayed_register = Some(register);
 229        let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
 230        replayer.replay(repeated_actions, window, cx);
 231    }
 232
 233    pub(crate) fn repeat(
 234        &mut self,
 235        from_insert_mode: bool,
 236        window: &mut Window,
 237        cx: &mut Context<Self>,
 238    ) {
 239        if self.active_operator().is_some() {
 240            Vim::update_globals(cx, |globals, _| {
 241                globals.recording_actions.clear();
 242                globals.recording_count = None;
 243                globals.dot_recording = false;
 244                globals.stop_recording_after_next_action = false;
 245            });
 246            self.clear_operator(window, cx);
 247            return;
 248        }
 249
 250        Vim::take_forced_motion(cx);
 251        let count = Vim::take_count(cx);
 252
 253        let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| {
 254            let actions = globals.recorded_actions.clone();
 255            if actions.is_empty() {
 256                return None;
 257            }
 258            if globals.replayer.is_none()
 259                && let Some(recording_register) = globals.recording_register
 260            {
 261                globals
 262                    .recordings
 263                    .entry(recording_register)
 264                    .or_default()
 265                    .push(ReplayableAction::Action(Repeat.boxed_clone()));
 266            }
 267
 268            let mut mode = None;
 269            let selection = globals.recorded_selection.clone();
 270            match selection {
 271                RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
 272                    globals.recorded_count = None;
 273                    mode = Some(Mode::Visual);
 274                }
 275                RecordedSelection::VisualLine { .. } => {
 276                    globals.recorded_count = None;
 277                    mode = Some(Mode::VisualLine)
 278                }
 279                RecordedSelection::VisualBlock { .. } => {
 280                    globals.recorded_count = None;
 281                    mode = Some(Mode::VisualBlock)
 282                }
 283                RecordedSelection::None => {
 284                    if let Some(count) = count {
 285                        globals.recorded_count = Some(count);
 286                    }
 287                }
 288            }
 289
 290            Some((actions, selection, mode))
 291        }) else {
 292            return;
 293        };
 294
 295        // Dot repeat always uses the recorded register, ignoring any "X
 296        // override, as the register is an inherent part of the recorded action.
 297        // For numbered registers, Neovim increments on each dot repeat so after
 298        // using `"1p`, using `.` will equate to `"2p", the next `.` to `"3p`,
 299        // etc..
 300        let recorded_register = cx.global::<VimGlobals>().recorded_register_for_dot;
 301        let next_register = recorded_register
 302            .filter(|c| matches!(c, '1'..='9'))
 303            .map(|c| ((c as u8 + 1).min(b'9')) as char);
 304
 305        self.selected_register = next_register.or(recorded_register);
 306        if let Some(next_register) = next_register {
 307            Vim::update_globals(cx, |globals, _| {
 308                globals.recorded_register_for_dot = Some(next_register)
 309            })
 310        };
 311
 312        if mode != Some(self.mode) {
 313            if let Some(mode) = mode {
 314                self.switch_mode(mode, false, window, cx)
 315            }
 316
 317            match selection {
 318                RecordedSelection::SingleLine { cols } => {
 319                    if cols > 1 {
 320                        self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
 321                    }
 322                }
 323                RecordedSelection::Visual { rows, cols } => {
 324                    self.visual_motion(
 325                        Motion::Down {
 326                            display_lines: false,
 327                        },
 328                        Some(rows as usize),
 329                        window,
 330                        cx,
 331                    );
 332                    self.visual_motion(
 333                        Motion::StartOfLine {
 334                            display_lines: false,
 335                        },
 336                        None,
 337                        window,
 338                        cx,
 339                    );
 340                    if cols > 1 {
 341                        self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
 342                    }
 343                }
 344                RecordedSelection::VisualBlock { rows, cols } => {
 345                    self.visual_motion(
 346                        Motion::Down {
 347                            display_lines: false,
 348                        },
 349                        Some(rows as usize),
 350                        window,
 351                        cx,
 352                    );
 353                    if cols > 1 {
 354                        self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx);
 355                    }
 356                }
 357                RecordedSelection::VisualLine { rows } => {
 358                    self.visual_motion(
 359                        Motion::Down {
 360                            display_lines: false,
 361                        },
 362                        Some(rows as usize),
 363                        window,
 364                        cx,
 365                    );
 366                }
 367                RecordedSelection::None => {}
 368            }
 369        }
 370
 371        // insert internally uses repeat to handle counts
 372        // vim doesn't treat 3a1 as though you literally repeated a1
 373        // 3 times, instead it inserts the content thrice at the insert position.
 374        if let Some(to_repeat) = repeatable_insert(&actions[0]) {
 375            if let Some(ReplayableAction::Action(action)) = actions.last()
 376                && NormalBefore.partial_eq(&**action)
 377            {
 378                actions.pop();
 379            }
 380
 381            let mut new_actions = actions.clone();
 382            actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
 383
 384            let mut count = cx.global::<VimGlobals>().recorded_count.unwrap_or(1);
 385
 386            // if we came from insert mode we're just doing repetitions 2 onwards.
 387            if from_insert_mode {
 388                count -= 1;
 389                new_actions[0] = actions[0].clone();
 390            }
 391
 392            for _ in 1..count {
 393                new_actions.append(actions.clone().as_mut());
 394            }
 395            new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
 396            actions = new_actions;
 397        }
 398
 399        actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
 400
 401        if self.temp_mode {
 402            self.temp_mode = false;
 403            actions.push(ReplayableAction::Action(InsertBefore.boxed_clone()));
 404        }
 405
 406        let globals = Vim::globals(cx);
 407        globals.dot_replaying = true;
 408        let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
 409
 410        replayer.replay(actions, window, cx);
 411    }
 412}
 413
 414#[cfg(test)]
 415mod test {
 416    use editor::test::editor_lsp_test_context::EditorLspTestContext;
 417    use futures::StreamExt;
 418    use indoc::indoc;
 419
 420    use gpui::EntityInputHandler;
 421
 422    use crate::{
 423        VimGlobals,
 424        state::Mode,
 425        test::{NeovimBackedTestContext, VimTestContext},
 426    };
 427
 428    #[gpui::test]
 429    async fn test_dot_repeat(cx: &mut gpui::TestAppContext) {
 430        let mut cx = NeovimBackedTestContext::new(cx).await;
 431
 432        // "o"
 433        cx.set_shared_state("ˇhello").await;
 434        cx.simulate_shared_keystrokes("o w o r l d escape").await;
 435        cx.shared_state().await.assert_eq("hello\nworlˇd");
 436        cx.simulate_shared_keystrokes(".").await;
 437        cx.shared_state().await.assert_eq("hello\nworld\nworlˇd");
 438
 439        // "d"
 440        cx.simulate_shared_keystrokes("^ d f o").await;
 441        cx.simulate_shared_keystrokes("g g .").await;
 442        cx.shared_state().await.assert_eq("ˇ\nworld\nrld");
 443
 444        // "p" (note that it pastes the current clipboard)
 445        cx.simulate_shared_keystrokes("j y y p").await;
 446        cx.simulate_shared_keystrokes("shift-g y y .").await;
 447        cx.shared_state()
 448            .await
 449            .assert_eq("\nworld\nworld\nrld\nˇrld");
 450
 451        // "~" (note that counts apply to the action taken, not . itself)
 452        cx.set_shared_state("ˇthe quick brown fox").await;
 453        cx.simulate_shared_keystrokes("2 ~ .").await;
 454        cx.set_shared_state("THE ˇquick brown fox").await;
 455        cx.simulate_shared_keystrokes("3 .").await;
 456        cx.set_shared_state("THE QUIˇck brown fox").await;
 457        cx.run_until_parked();
 458        cx.simulate_shared_keystrokes(".").await;
 459        cx.shared_state().await.assert_eq("THE QUICK ˇbrown fox");
 460    }
 461
 462    #[gpui::test]
 463    async fn test_dot_repeat_registers_paste(cx: &mut gpui::TestAppContext) {
 464        let mut cx = NeovimBackedTestContext::new(cx).await;
 465
 466        // basic paste repeat uses the unnamed register
 467        cx.set_shared_state("ˇhello\n").await;
 468        cx.simulate_shared_keystrokes("y y p").await;
 469        cx.shared_state().await.assert_eq("hello\nˇhello\n");
 470        cx.simulate_shared_keystrokes(".").await;
 471        cx.shared_state().await.assert_eq("hello\nhello\nˇhello\n");
 472
 473        // "_ (blackhole) is recorded and replayed, so the pasted text is still
 474        // the original yanked line.
 475        cx.set_shared_state(indoc! {"
 476            ˇone
 477            two
 478            three
 479            four
 480        "})
 481            .await;
 482        cx.simulate_shared_keystrokes("y y j \" _ d d . p").await;
 483        cx.shared_state().await.assert_eq(indoc! {"
 484            one
 485            four
 486            ˇone
 487        "});
 488
 489        // the recorded register is replayed, not whatever is in the unnamed register
 490        cx.set_shared_state(indoc! {"
 491            ˇone
 492            two
 493        "})
 494            .await;
 495        cx.simulate_shared_keystrokes("y y j \" a y y \" a p .")
 496            .await;
 497        cx.shared_state().await.assert_eq(indoc! {"
 498            one
 499            two
 500            two
 501            ˇtwo
 502        "});
 503
 504        // `"X.` ignores the override and always uses the recorded register.
 505        // Both `dd` calls go into register `a`, so register `b` is empty and
 506        // `"bp` pastes nothing.
 507        cx.set_shared_state(indoc! {"
 508            ˇone
 509            two
 510            three
 511        "})
 512            .await;
 513        cx.simulate_shared_keystrokes("\" a d d \" b .").await;
 514        cx.shared_state().await.assert_eq(indoc! {"
 515            ˇthree
 516        "});
 517        cx.simulate_shared_keystrokes("\" a p \" b p").await;
 518        cx.shared_state().await.assert_eq(indoc! {"
 519            three
 520            ˇtwo
 521        "});
 522
 523        // numbered registers cycle on each dot repeat: "1p . . uses registers 2, 3, …
 524        // Since the cycling behavior caps at register 9, the first line to be
 525        // deleted `1`, is no longer in any of the registers.
 526        cx.set_shared_state(indoc! {"
 527            ˇone
 528            two
 529            three
 530            four
 531            five
 532            six
 533            seven
 534            eight
 535            nine
 536            ten
 537        "})
 538            .await;
 539        cx.simulate_shared_keystrokes("d d . . . . . . . . .").await;
 540        cx.shared_state().await.assert_eq(indoc! {"ˇ"});
 541        cx.simulate_shared_keystrokes("\" 1 p . . . . . . . . .")
 542            .await;
 543        cx.shared_state().await.assert_eq(indoc! {"
 544
 545            ten
 546            nine
 547            eight
 548            seven
 549            six
 550            five
 551            four
 552            three
 553            two
 554            ˇtwo"});
 555
 556        // unnamed register repeat: dd records None, so . pastes the same
 557        // deleted text
 558        cx.set_shared_state(indoc! {"
 559            ˇone
 560            two
 561            three
 562        "})
 563            .await;
 564        cx.simulate_shared_keystrokes("d d p .").await;
 565        cx.shared_state().await.assert_eq(indoc! {"
 566            two
 567            one
 568            ˇone
 569            three
 570        "});
 571
 572        // After `"1p` cycles to `2`, using `"ap` resets recorded_register to `a`,
 573        // so the next `.` uses `a` and not 3.
 574        cx.set_shared_state(indoc! {"
 575            one
 576            two
 577            ˇthree
 578        "})
 579            .await;
 580        cx.simulate_shared_keystrokes("\" 2 y y k k \" a y y j \" 1 y y k \" 1 p . \" a p .")
 581            .await;
 582        cx.shared_state().await.assert_eq(indoc! {"
 583            one
 584            two
 585            three
 586            one
 587            ˇone
 588            two
 589            three
 590        "});
 591    }
 592
 593    // This needs to be a separate test from `test_dot_repeat_registers_paste`
 594    // as Neovim doesn't have support for using registers in replace operations
 595    // by default.
 596    #[gpui::test]
 597    async fn test_dot_repeat_registers_replace(cx: &mut gpui::TestAppContext) {
 598        let mut cx = VimTestContext::new(cx, true).await;
 599
 600        cx.set_state(
 601            indoc! {"
 602            line ˇone
 603            line two
 604            line three
 605        "},
 606            Mode::Normal,
 607        );
 608
 609        // 1. Yank `one` into register `a`
 610        // 2. Move down and yank `two` into the default register
 611        // 3. Replace `two` with the contents of register `a`
 612        cx.simulate_keystrokes("\" a y w j y w \" a g R w");
 613        cx.assert_state(
 614            indoc! {"
 615            line one
 616            line onˇe
 617            line three
 618        "},
 619            Mode::Normal,
 620        );
 621
 622        // 1. Move down to `three`
 623        // 2. Repeat the replace operation
 624        cx.simulate_keystrokes("j .");
 625        cx.assert_state(
 626            indoc! {"
 627            line one
 628            line one
 629            line onˇe
 630        "},
 631            Mode::Normal,
 632        );
 633
 634        // Similar test, but this time using numbered registers, as those should
 635        // automatically increase on successive uses of `.` .
 636        cx.set_state(
 637            indoc! {"
 638            line ˇone
 639            line two
 640            line three
 641            line four
 642        "},
 643            Mode::Normal,
 644        );
 645
 646        // 1. Yank `one` into register `1`
 647        // 2. Yank `two` into register `2`
 648        // 3. Move down and yank `three` into the default register
 649        // 4. Replace `three` with the contents of register `1`
 650        // 5. Move down and repeat
 651        cx.simulate_keystrokes("\" 1 y w j \" 2 y w j y w \" 1 g R w j .");
 652        cx.assert_state(
 653            indoc! {"
 654            line one
 655            line two
 656            line one
 657            line twˇo
 658        "},
 659            Mode::Normal,
 660        );
 661    }
 662
 663    #[gpui::test]
 664    async fn test_repeat_ime(cx: &mut gpui::TestAppContext) {
 665        let mut cx = VimTestContext::new(cx, true).await;
 666
 667        cx.set_state("hˇllo", Mode::Normal);
 668        cx.simulate_keystrokes("i");
 669
 670        // simulate brazilian input for ä.
 671        cx.update_editor(|editor, window, cx| {
 672            editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), window, cx);
 673            editor.replace_text_in_range(None, "ä", window, cx);
 674        });
 675        cx.simulate_keystrokes("escape");
 676        cx.assert_state("hˇällo", Mode::Normal);
 677        cx.simulate_keystrokes(".");
 678        cx.assert_state("hˇäällo", Mode::Normal);
 679    }
 680
 681    #[gpui::test]
 682    async fn test_repeat_completion(cx: &mut gpui::TestAppContext) {
 683        VimTestContext::init(cx);
 684        let cx = EditorLspTestContext::new_rust(
 685            lsp::ServerCapabilities {
 686                completion_provider: Some(lsp::CompletionOptions {
 687                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
 688                    resolve_provider: Some(true),
 689                    ..Default::default()
 690                }),
 691                ..Default::default()
 692            },
 693            cx,
 694        )
 695        .await;
 696        let mut cx = VimTestContext::new_with_lsp(cx, true);
 697
 698        cx.set_state(
 699            indoc! {"
 700            onˇe
 701            two
 702            three
 703        "},
 704            Mode::Normal,
 705        );
 706
 707        let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
 708            move |_, params, _| async move {
 709                let position = params.text_document_position.position;
 710                Ok(Some(lsp::CompletionResponse::Array(vec![
 711                    lsp::CompletionItem {
 712                        label: "first".to_string(),
 713                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 714                            range: lsp::Range::new(position, position),
 715                            new_text: "first".to_string(),
 716                        })),
 717                        ..Default::default()
 718                    },
 719                    lsp::CompletionItem {
 720                        label: "second".to_string(),
 721                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 722                            range: lsp::Range::new(position, position),
 723                            new_text: "second".to_string(),
 724                        })),
 725                        ..Default::default()
 726                    },
 727                ])))
 728            },
 729        );
 730        cx.simulate_keystrokes("a .");
 731        request.next().await;
 732        cx.condition(|editor, _| editor.context_menu_visible())
 733            .await;
 734        cx.simulate_keystrokes("down enter ! escape");
 735
 736        cx.assert_state(
 737            indoc! {"
 738                one.secondˇ!
 739                two
 740                three
 741            "},
 742            Mode::Normal,
 743        );
 744        cx.simulate_keystrokes("j .");
 745        cx.assert_state(
 746            indoc! {"
 747                one.second!
 748                two.secondˇ!
 749                three
 750            "},
 751            Mode::Normal,
 752        );
 753    }
 754
 755    #[gpui::test]
 756    async fn test_repeat_completion_unicode_bug(cx: &mut gpui::TestAppContext) {
 757        VimTestContext::init(cx);
 758        let cx = EditorLspTestContext::new_rust(
 759            lsp::ServerCapabilities {
 760                completion_provider: Some(lsp::CompletionOptions {
 761                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
 762                    resolve_provider: Some(true),
 763                    ..Default::default()
 764                }),
 765                ..Default::default()
 766            },
 767            cx,
 768        )
 769        .await;
 770        let mut cx = VimTestContext::new_with_lsp(cx, true);
 771
 772        cx.set_state(
 773            indoc! {"
 774                ĩлˇк
 775                ĩлк
 776            "},
 777            Mode::Normal,
 778        );
 779
 780        let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(
 781            move |_, params, _| async move {
 782                let position = params.text_document_position.position;
 783                let mut to_the_left = position;
 784                to_the_left.character -= 2;
 785                Ok(Some(lsp::CompletionResponse::Array(vec![
 786                    lsp::CompletionItem {
 787                        label: "oops".to_string(),
 788                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 789                            range: lsp::Range::new(to_the_left, position),
 790                            new_text: "к!".to_string(),
 791                        })),
 792                        ..Default::default()
 793                    },
 794                ])))
 795            },
 796        );
 797        cx.simulate_keystrokes("i .");
 798        request.next().await;
 799        cx.condition(|editor, _| editor.context_menu_visible())
 800            .await;
 801        cx.simulate_keystrokes("enter escape");
 802        cx.assert_state(
 803            indoc! {"
 804                ĩкˇ!к
 805                ĩлк
 806            "},
 807            Mode::Normal,
 808        );
 809    }
 810
 811    #[gpui::test]
 812    async fn test_repeat_visual(cx: &mut gpui::TestAppContext) {
 813        let mut cx = NeovimBackedTestContext::new(cx).await;
 814
 815        // single-line (3 columns)
 816        cx.set_shared_state(indoc! {
 817            "ˇthe quick brown
 818            fox jumps over
 819            the lazy dog"
 820        })
 821        .await;
 822        cx.simulate_shared_keystrokes("v i w s o escape").await;
 823        cx.shared_state().await.assert_eq(indoc! {
 824            "ˇo quick brown
 825            fox jumps over
 826            the lazy dog"
 827        });
 828        cx.simulate_shared_keystrokes("j w .").await;
 829        cx.shared_state().await.assert_eq(indoc! {
 830            "o quick brown
 831            fox ˇops over
 832            the lazy dog"
 833        });
 834        cx.simulate_shared_keystrokes("f r .").await;
 835        cx.shared_state().await.assert_eq(indoc! {
 836            "o quick brown
 837            fox ops oveˇothe lazy dog"
 838        });
 839
 840        // visual
 841        cx.set_shared_state(indoc! {
 842            "the ˇquick brown
 843            fox jumps over
 844            fox jumps over
 845            fox jumps over
 846            the lazy dog"
 847        })
 848        .await;
 849        cx.simulate_shared_keystrokes("v j x").await;
 850        cx.shared_state().await.assert_eq(indoc! {
 851            "the ˇumps over
 852            fox jumps over
 853            fox jumps over
 854            the lazy dog"
 855        });
 856        cx.simulate_shared_keystrokes(".").await;
 857        cx.shared_state().await.assert_eq(indoc! {
 858            "the ˇumps over
 859            fox jumps over
 860            the lazy dog"
 861        });
 862        cx.simulate_shared_keystrokes("w .").await;
 863        cx.shared_state().await.assert_eq(indoc! {
 864            "the umps ˇumps over
 865            the lazy dog"
 866        });
 867        cx.simulate_shared_keystrokes("j .").await;
 868        cx.shared_state().await.assert_eq(indoc! {
 869            "the umps umps over
 870            the ˇog"
 871        });
 872
 873        // block mode (3 rows)
 874        cx.set_shared_state(indoc! {
 875            "ˇthe quick brown
 876            fox jumps over
 877            the lazy dog"
 878        })
 879        .await;
 880        cx.simulate_shared_keystrokes("ctrl-v j j shift-i o escape")
 881            .await;
 882        cx.shared_state().await.assert_eq(indoc! {
 883            "ˇothe quick brown
 884            ofox jumps over
 885            othe lazy dog"
 886        });
 887        cx.simulate_shared_keystrokes("j 4 l .").await;
 888        cx.shared_state().await.assert_eq(indoc! {
 889            "othe quick brown
 890            ofoxˇo jumps over
 891            otheo lazy dog"
 892        });
 893
 894        // line mode
 895        cx.set_shared_state(indoc! {
 896            "ˇthe quick brown
 897            fox jumps over
 898            the lazy dog"
 899        })
 900        .await;
 901        cx.simulate_shared_keystrokes("shift-v shift-r o escape")
 902            .await;
 903        cx.shared_state().await.assert_eq(indoc! {
 904            "ˇo
 905            fox jumps over
 906            the lazy dog"
 907        });
 908        cx.simulate_shared_keystrokes("j .").await;
 909        cx.shared_state().await.assert_eq(indoc! {
 910            "o
 911            ˇo
 912            the lazy dog"
 913        });
 914    }
 915
 916    #[gpui::test]
 917    async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) {
 918        let mut cx = NeovimBackedTestContext::new(cx).await;
 919
 920        cx.set_shared_state(indoc! {
 921            "ˇthe quick brown
 922            fox jumps over
 923            the lazy dog"
 924        })
 925        .await;
 926        cx.simulate_shared_keystrokes("3 d 3 l").await;
 927        cx.shared_state().await.assert_eq(indoc! {
 928            "ˇ brown
 929            fox jumps over
 930            the lazy dog"
 931        });
 932        cx.simulate_shared_keystrokes("j .").await;
 933        cx.shared_state().await.assert_eq(indoc! {
 934            " brown
 935            ˇ over
 936            the lazy dog"
 937        });
 938        cx.simulate_shared_keystrokes("j 2 .").await;
 939        cx.shared_state().await.assert_eq(indoc! {
 940            " brown
 941             over
 942            ˇe lazy dog"
 943        });
 944    }
 945
 946    #[gpui::test]
 947    async fn test_record_interrupted(cx: &mut gpui::TestAppContext) {
 948        let mut cx = VimTestContext::new(cx, true).await;
 949
 950        cx.set_state("ˇhello\n", Mode::Normal);
 951        cx.simulate_keystrokes("4 i j cmd-shift-p escape");
 952        cx.simulate_keystrokes("escape");
 953        cx.assert_state("ˇjhello\n", Mode::Normal);
 954    }
 955
 956    #[gpui::test]
 957    async fn test_repeat_over_blur(cx: &mut gpui::TestAppContext) {
 958        let mut cx = NeovimBackedTestContext::new(cx).await;
 959
 960        cx.set_shared_state("ˇhello hello hello\n").await;
 961        cx.simulate_shared_keystrokes("c f o x escape").await;
 962        cx.shared_state().await.assert_eq("ˇx hello hello\n");
 963        cx.simulate_shared_keystrokes(": escape").await;
 964        cx.simulate_shared_keystrokes(".").await;
 965        cx.shared_state().await.assert_eq("ˇx hello\n");
 966    }
 967
 968    #[gpui::test]
 969    async fn test_repeat_after_blur_resets_dot_replaying(cx: &mut gpui::TestAppContext) {
 970        let mut cx = VimTestContext::new(cx, true).await;
 971
 972        // Bind `ctrl-f` to the `buffer_search::Deploy` action so that this can
 973        // be triggered while in Insert mode, ensuring that an action which
 974        // moves the focus away from the editor, gets recorded.
 975        cx.update(|_, cx| {
 976            cx.bind_keys([gpui::KeyBinding::new(
 977                "ctrl-f",
 978                search::buffer_search::Deploy::find(),
 979                None,
 980            )])
 981        });
 982
 983        cx.set_state("ˇhello", Mode::Normal);
 984
 985        // We're going to enter insert mode, which will start recording, type a
 986        // character and then immediately use `ctrl-f` to trigger the buffer
 987        // search. Triggering the buffer search will move focus away from the
 988        // editor, effectively stopping the recording immediately after
 989        // `buffer_search::Deploy` is recorded. The first `escape` is used to
 990        // dismiss the search bar, while the second is used to move from Insert
 991        // to Normal mode.
 992        cx.simulate_keystrokes("i x ctrl-f escape escape");
 993        cx.run_until_parked();
 994
 995        // Using the `.` key will dispatch the `vim::Repeat` action, repeating
 996        // the set of recorded actions. This will eventually focus on the search
 997        // bar, preventing the `EndRepeat` action from being correctly handled.
 998        cx.simulate_keystrokes(".");
 999        cx.run_until_parked();
1000
1001        // After replay finishes, even though the `EndRepeat` action wasn't
1002        // handled, seeing as the editor lost focus during replay, the
1003        // `dot_replaying` value should be set back to `false`.
1004        assert!(
1005            !cx.update(|_, cx| cx.global::<VimGlobals>().dot_replaying),
1006            "dot_replaying should be false after repeat completes"
1007        );
1008    }
1009
1010    #[gpui::test]
1011    async fn test_undo_repeated_insert(cx: &mut gpui::TestAppContext) {
1012        let mut cx = NeovimBackedTestContext::new(cx).await;
1013
1014        cx.set_shared_state("hellˇo").await;
1015        cx.simulate_shared_keystrokes("3 a . escape").await;
1016        cx.shared_state().await.assert_eq("hello..ˇ.");
1017        cx.simulate_shared_keystrokes("u").await;
1018        cx.shared_state().await.assert_eq("hellˇo");
1019    }
1020
1021    #[gpui::test]
1022    async fn test_record_replay(cx: &mut gpui::TestAppContext) {
1023        let mut cx = NeovimBackedTestContext::new(cx).await;
1024
1025        cx.set_shared_state("ˇhello world").await;
1026        cx.simulate_shared_keystrokes("q w c w j escape q").await;
1027        cx.shared_state().await.assert_eq("ˇj world");
1028        cx.simulate_shared_keystrokes("2 l @ w").await;
1029        cx.shared_state().await.assert_eq("j ˇj");
1030    }
1031
1032    #[gpui::test]
1033    async fn test_record_replay_count(cx: &mut gpui::TestAppContext) {
1034        let mut cx = NeovimBackedTestContext::new(cx).await;
1035
1036        cx.set_shared_state("ˇhello world!!").await;
1037        cx.simulate_shared_keystrokes("q a v 3 l s 0 escape l q")
1038            .await;
1039        cx.shared_state().await.assert_eq("0ˇo world!!");
1040        cx.simulate_shared_keystrokes("2 @ a").await;
1041        cx.shared_state().await.assert_eq("000ˇ!");
1042    }
1043
1044    #[gpui::test]
1045    async fn test_record_replay_dot(cx: &mut gpui::TestAppContext) {
1046        let mut cx = NeovimBackedTestContext::new(cx).await;
1047
1048        cx.set_shared_state("ˇhello world").await;
1049        cx.simulate_shared_keystrokes("q a r a l r b l q").await;
1050        cx.shared_state().await.assert_eq("abˇllo world");
1051        cx.simulate_shared_keystrokes(".").await;
1052        cx.shared_state().await.assert_eq("abˇblo world");
1053        cx.simulate_shared_keystrokes("shift-q").await;
1054        cx.shared_state().await.assert_eq("ababˇo world");
1055        cx.simulate_shared_keystrokes(".").await;
1056        cx.shared_state().await.assert_eq("ababˇb world");
1057    }
1058
1059    #[gpui::test]
1060    async fn test_record_replay_of_dot(cx: &mut gpui::TestAppContext) {
1061        let mut cx = NeovimBackedTestContext::new(cx).await;
1062
1063        cx.set_shared_state("ˇhello world").await;
1064        cx.simulate_shared_keystrokes("r o q w . q").await;
1065        cx.shared_state().await.assert_eq("ˇoello world");
1066        cx.simulate_shared_keystrokes("d l").await;
1067        cx.shared_state().await.assert_eq("ˇello world");
1068        cx.simulate_shared_keystrokes("@ w").await;
1069        cx.shared_state().await.assert_eq("ˇllo world");
1070    }
1071
1072    #[gpui::test]
1073    async fn test_record_replay_interleaved(cx: &mut gpui::TestAppContext) {
1074        let mut cx = NeovimBackedTestContext::new(cx).await;
1075
1076        cx.set_shared_state("ˇhello world").await;
1077        cx.simulate_shared_keystrokes("q z r a l q").await;
1078        cx.shared_state().await.assert_eq("aˇello world");
1079        cx.simulate_shared_keystrokes("q b @ z @ z q").await;
1080        cx.shared_state().await.assert_eq("aaaˇlo world");
1081        cx.simulate_shared_keystrokes("@ @").await;
1082        cx.shared_state().await.assert_eq("aaaaˇo world");
1083        cx.simulate_shared_keystrokes("@ b").await;
1084        cx.shared_state().await.assert_eq("aaaaaaˇworld");
1085        cx.simulate_shared_keystrokes("@ @").await;
1086        cx.shared_state().await.assert_eq("aaaaaaaˇorld");
1087        cx.simulate_shared_keystrokes("q z r b l q").await;
1088        cx.shared_state().await.assert_eq("aaaaaaabˇrld");
1089        cx.simulate_shared_keystrokes("@ b").await;
1090        cx.shared_state().await.assert_eq("aaaaaaabbbˇd");
1091    }
1092
1093    #[gpui::test]
1094    async fn test_repeat_clear(cx: &mut gpui::TestAppContext) {
1095        let mut cx = VimTestContext::new(cx, true).await;
1096
1097        // Check that, when repeat is preceded by something other than a number,
1098        // the current operator is cleared, in order to prevent infinite loops.
1099        cx.set_state("ˇhello world", Mode::Normal);
1100        cx.simulate_keystrokes("d .");
1101        assert_eq!(cx.active_operator(), None);
1102    }
1103
1104    #[gpui::test]
1105    async fn test_repeat_clear_repeat(cx: &mut gpui::TestAppContext) {
1106        let mut cx = NeovimBackedTestContext::new(cx).await;
1107
1108        cx.set_shared_state(indoc! {
1109            "ˇthe quick brown
1110            fox jumps over
1111            the lazy dog"
1112        })
1113        .await;
1114        cx.simulate_shared_keystrokes("d d").await;
1115        cx.shared_state().await.assert_eq(indoc! {
1116            "ˇfox jumps over
1117            the lazy dog"
1118        });
1119        cx.simulate_shared_keystrokes("d . .").await;
1120        cx.shared_state().await.assert_eq(indoc! {
1121            "ˇthe lazy dog"
1122        });
1123    }
1124
1125    #[gpui::test]
1126    async fn test_repeat_clear_count(cx: &mut gpui::TestAppContext) {
1127        let mut cx = NeovimBackedTestContext::new(cx).await;
1128
1129        cx.set_shared_state(indoc! {
1130            "ˇthe quick brown
1131            fox jumps over
1132            the lazy dog"
1133        })
1134        .await;
1135        cx.simulate_shared_keystrokes("d d").await;
1136        cx.shared_state().await.assert_eq(indoc! {
1137            "ˇfox jumps over
1138            the lazy dog"
1139        });
1140        cx.simulate_shared_keystrokes("2 d .").await;
1141        cx.shared_state().await.assert_eq(indoc! {
1142            "ˇfox jumps over
1143            the lazy dog"
1144        });
1145        cx.simulate_shared_keystrokes(".").await;
1146        cx.shared_state().await.assert_eq(indoc! {
1147            "ˇthe lazy dog"
1148        });
1149
1150        cx.set_shared_state(indoc! {
1151            "ˇthe quick brown
1152            fox jumps over
1153            the lazy dog
1154            the quick brown
1155            fox jumps over
1156            the lazy dog"
1157        })
1158        .await;
1159        cx.simulate_shared_keystrokes("2 d d").await;
1160        cx.shared_state().await.assert_eq(indoc! {
1161            "ˇthe lazy dog
1162            the quick brown
1163            fox jumps over
1164            the lazy dog"
1165        });
1166        cx.simulate_shared_keystrokes("5 d .").await;
1167        cx.shared_state().await.assert_eq(indoc! {
1168            "ˇthe lazy dog
1169            the quick brown
1170            fox jumps over
1171            the lazy dog"
1172        });
1173        cx.simulate_shared_keystrokes(".").await;
1174        cx.shared_state().await.assert_eq(indoc! {
1175            "ˇfox jumps over
1176            the lazy dog"
1177        });
1178    }
1179}