paste.rs

   1use editor::{
   2    DisplayPoint, MultiBufferOffset, RowExt, SelectionEffects, display_map::ToDisplayPoint,
   3    movement,
   4};
   5use gpui::{Action, Context, Window};
   6use language::{Bias, SelectionGoal};
   7use schemars::JsonSchema;
   8use serde::Deserialize;
   9use settings::Settings;
  10use std::cmp;
  11use vim_mode_setting::HelixModeSetting;
  12
  13use crate::{
  14    Vim,
  15    motion::{Motion, MotionKind},
  16    object::Object,
  17    state::{Mode, Register},
  18};
  19
  20/// Pastes text from the specified register at the cursor position.
  21#[derive(Clone, Default, Deserialize, JsonSchema, PartialEq, Action)]
  22#[action(namespace = vim)]
  23#[serde(deny_unknown_fields)]
  24pub struct Paste {
  25    #[serde(default)]
  26    before: bool,
  27    #[serde(default)]
  28    preserve_clipboard: bool,
  29}
  30
  31impl Vim {
  32    pub fn paste(&mut self, action: &Paste, window: &mut Window, cx: &mut Context<Self>) {
  33        self.record_current_action(cx);
  34        self.store_visual_marks(window, cx);
  35        let count = Vim::take_count(cx).unwrap_or(1);
  36        Vim::take_forced_motion(cx);
  37
  38        self.update_editor(cx, |vim, editor, cx| {
  39            let text_layout_details = editor.text_layout_details(window, cx);
  40            editor.transact(window, cx, |editor, window, cx| {
  41                editor.set_clip_at_line_ends(false, cx);
  42
  43                let selected_register = vim.selected_register.take();
  44
  45                let Some(Register {
  46                    text,
  47                    clipboard_selections,
  48                }) = Vim::update_globals(cx, |globals, cx| {
  49                    globals.read_register(selected_register, Some(editor), cx)
  50                })
  51                .filter(|reg| !reg.text.is_empty())
  52                else {
  53                    vim.set_status_label(
  54                        format!("Nothing in register {}", selected_register.unwrap_or('"')),
  55                        cx,
  56                    );
  57                    return;
  58                };
  59                let clipboard_selections = clipboard_selections
  60                    .filter(|sel| sel.len() > 1 && vim.mode != Mode::VisualLine);
  61
  62                if !action.preserve_clipboard && vim.mode.is_visual() {
  63                    vim.copy_selections_content(editor, MotionKind::for_mode(vim.mode), window, cx);
  64                }
  65
  66                let display_map = editor.display_snapshot(cx);
  67                let current_selections = editor.selections.all_adjusted_display(&display_map);
  68
  69                // unlike zed, if you have a multi-cursor selection from vim block mode,
  70                // pasting it will paste it on subsequent lines, even if you don't yet
  71                // have a cursor there.
  72                let mut selections_to_process = Vec::new();
  73                let mut i = 0;
  74                while i < current_selections.len() {
  75                    selections_to_process
  76                        .push((current_selections[i].start..current_selections[i].end, true));
  77                    i += 1;
  78                }
  79                if let Some(clipboard_selections) = clipboard_selections.as_ref() {
  80                    let left = current_selections
  81                        .iter()
  82                        .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
  83                        .min()
  84                        .unwrap();
  85                    let mut row = current_selections.last().unwrap().end.row().next_row();
  86                    while i < clipboard_selections.len() {
  87                        let cursor =
  88                            display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
  89                        selections_to_process.push((cursor..cursor, false));
  90                        i += 1;
  91                        row.0 += 1;
  92                    }
  93                }
  94
  95                let first_selection_indent_column =
  96                    clipboard_selections.as_ref().and_then(|zed_selections| {
  97                        zed_selections
  98                            .first()
  99                            .map(|selection| selection.first_line_indent)
 100                    });
 101                let before = action.before || vim.mode == Mode::VisualLine;
 102
 103                let mut edits = Vec::new();
 104                let mut new_selections = Vec::new();
 105                let mut original_indent_columns = Vec::new();
 106                let mut start_offset = 0;
 107
 108                for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
 109                    let (mut to_insert, original_indent_column) =
 110                        if let Some(clipboard_selections) = &clipboard_selections {
 111                            if let Some(clipboard_selection) = clipboard_selections.get(ix) {
 112                                let end_offset = start_offset + clipboard_selection.len;
 113                                let text = text[start_offset..end_offset].to_string();
 114                                start_offset = if clipboard_selection.is_entire_line {
 115                                    end_offset
 116                                } else {
 117                                    end_offset + 1
 118                                };
 119                                (text, Some(clipboard_selection.first_line_indent))
 120                            } else {
 121                                ("".to_string(), first_selection_indent_column)
 122                            }
 123                        } else {
 124                            (text.to_string(), first_selection_indent_column)
 125                        };
 126                    let line_mode = to_insert.ends_with('\n');
 127                    let is_multiline = to_insert.contains('\n');
 128
 129                    if line_mode && !before {
 130                        if selection.is_empty() {
 131                            to_insert =
 132                                "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
 133                        } else {
 134                            to_insert = "\n".to_owned() + &to_insert;
 135                        }
 136                    } else if line_mode && vim.mode == Mode::VisualLine {
 137                        to_insert.pop();
 138                    }
 139
 140                    let display_range = if !selection.is_empty() {
 141                        // If vim is in VISUAL LINE mode and the column for the
 142                        // selection's end point is 0, that means that the
 143                        // cursor is at the newline character (\n) at the end of
 144                        // the line. In this situation we'll want to move one
 145                        // position to the left, ensuring we don't join the last
 146                        // line of the selection with the line directly below.
 147                        let end_point =
 148                            if vim.mode == Mode::VisualLine && selection.end.column() == 0 {
 149                                movement::left(&display_map, selection.end)
 150                            } else {
 151                                selection.end
 152                            };
 153
 154                        selection.start..end_point
 155                    } else if line_mode {
 156                        let point = if before {
 157                            movement::line_beginning(&display_map, selection.start, false)
 158                        } else {
 159                            movement::line_end(&display_map, selection.start, false)
 160                        };
 161                        point..point
 162                    } else {
 163                        let point = if before {
 164                            selection.start
 165                        } else {
 166                            movement::saturating_right(&display_map, selection.start)
 167                        };
 168                        point..point
 169                    };
 170
 171                    let point_range = display_range.start.to_point(&display_map)
 172                        ..display_range.end.to_point(&display_map);
 173                    let anchor = if is_multiline || vim.mode == Mode::VisualLine {
 174                        display_map
 175                            .buffer_snapshot()
 176                            .anchor_before(point_range.start)
 177                    } else {
 178                        display_map.buffer_snapshot().anchor_after(point_range.end)
 179                    };
 180
 181                    if *preserve {
 182                        new_selections.push((anchor, line_mode, is_multiline));
 183                    }
 184                    edits.push((point_range, to_insert.repeat(count)));
 185                    original_indent_columns.push(original_indent_column);
 186                }
 187
 188                let cursor_offset = editor
 189                    .selections
 190                    .last::<MultiBufferOffset>(&display_map)
 191                    .head();
 192                if editor
 193                    .buffer()
 194                    .read(cx)
 195                    .snapshot(cx)
 196                    .language_settings_at(cursor_offset, cx)
 197                    .auto_indent_on_paste
 198                {
 199                    editor.edit_with_block_indent(edits, original_indent_columns, cx);
 200                } else {
 201                    editor.edit(edits, cx);
 202                }
 203
 204                // in line_mode vim will insert the new text on the next (or previous if before) line
 205                // and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
 206                // otherwise vim will insert the next text at (or before) the current cursor position,
 207                // the cursor will go to the last (or first, if is_multiline) inserted character.
 208                editor.change_selections(Default::default(), window, cx, |s| {
 209                    s.replace_cursors_with(|map| {
 210                        let mut cursors = Vec::new();
 211                        for (anchor, line_mode, is_multiline) in &new_selections {
 212                            let mut cursor = anchor.to_display_point(map);
 213                            if *line_mode {
 214                                if !before {
 215                                    cursor = movement::down(
 216                                        map,
 217                                        cursor,
 218                                        SelectionGoal::None,
 219                                        false,
 220                                        &text_layout_details,
 221                                    )
 222                                    .0;
 223                                }
 224                                cursor = movement::indented_line_beginning(map, cursor, true, true);
 225                            } else if !is_multiline && !vim.temp_mode {
 226                                cursor = movement::saturating_left(map, cursor)
 227                            }
 228                            cursors.push(cursor);
 229                            if vim.mode == Mode::VisualBlock {
 230                                break;
 231                            }
 232                        }
 233
 234                        cursors
 235                    });
 236                })
 237            });
 238        });
 239
 240        if HelixModeSetting::get_global(cx).0 {
 241            self.switch_mode(Mode::HelixNormal, true, window, cx);
 242        } else {
 243            self.switch_mode(Mode::Normal, true, window, cx);
 244        }
 245    }
 246
 247    pub fn replace_with_register_object(
 248        &mut self,
 249        object: Object,
 250        around: bool,
 251        window: &mut Window,
 252        cx: &mut Context<Self>,
 253    ) {
 254        self.stop_recording(cx);
 255        let selected_register = self.selected_register.take();
 256        self.update_editor(cx, |vim, editor, cx| {
 257            editor.transact(window, cx, |editor, window, cx| {
 258                editor.set_clip_at_line_ends(false, cx);
 259                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 260                    s.move_with(&mut |map, selection| {
 261                        object.expand_selection(map, selection, around, None);
 262                    });
 263                });
 264
 265                let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| {
 266                    globals.read_register(selected_register, Some(editor), cx)
 267                })
 268                .filter(|reg| !reg.text.is_empty()) else {
 269                    vim.set_status_label(
 270                        format!("Nothing in register {}", selected_register.unwrap_or('"')),
 271                        cx,
 272                    );
 273                    return;
 274                };
 275                editor.insert(&text, window, cx);
 276                editor.set_clip_at_line_ends(true, cx);
 277                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 278                    s.move_with(&mut |map, selection| {
 279                        selection.start = map.clip_point(selection.start, Bias::Left);
 280                        selection.end = selection.start
 281                    })
 282                })
 283            });
 284        });
 285    }
 286
 287    pub fn replace_with_register_motion(
 288        &mut self,
 289        motion: Motion,
 290        times: Option<usize>,
 291        forced_motion: bool,
 292        window: &mut Window,
 293        cx: &mut Context<Self>,
 294    ) {
 295        self.stop_recording(cx);
 296        let selected_register = self.selected_register.take();
 297        self.update_editor(cx, |vim, editor, cx| {
 298            let text_layout_details = editor.text_layout_details(window, cx);
 299            editor.transact(window, cx, |editor, window, cx| {
 300                editor.set_clip_at_line_ends(false, cx);
 301                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 302                    s.move_with(&mut |map, selection| {
 303                        motion.expand_selection(
 304                            map,
 305                            selection,
 306                            times,
 307                            &text_layout_details,
 308                            forced_motion,
 309                        );
 310                    });
 311                });
 312
 313                let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| {
 314                    globals.read_register(selected_register, Some(editor), cx)
 315                })
 316                .filter(|reg| !reg.text.is_empty()) else {
 317                    vim.set_status_label(
 318                        format!("Nothing in register {}", selected_register.unwrap_or('"')),
 319                        cx,
 320                    );
 321                    return;
 322                };
 323                editor.insert(&text, window, cx);
 324                editor.set_clip_at_line_ends(true, cx);
 325                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 326                    s.move_with(&mut |map, selection| {
 327                        selection.start = map.clip_point(selection.start, Bias::Left);
 328                        selection.end = selection.start
 329                    })
 330                })
 331            });
 332        });
 333    }
 334}
 335
 336#[cfg(test)]
 337mod test {
 338    use crate::{
 339        state::{Mode, Register},
 340        test::{NeovimBackedTestContext, VimTestContext},
 341    };
 342    use gpui::ClipboardItem;
 343    use indoc::indoc;
 344    use language::{LanguageName, language_settings::LanguageSettingsContent};
 345    use settings::{SettingsStore, UseSystemClipboard};
 346
 347    #[gpui::test]
 348    async fn test_paste(cx: &mut gpui::TestAppContext) {
 349        let mut cx = NeovimBackedTestContext::new(cx).await;
 350
 351        // single line
 352        cx.set_shared_state(indoc! {"
 353            The quick brown
 354            fox ˇjumps over
 355            the lazy dog"})
 356            .await;
 357        cx.simulate_shared_keystrokes("v w y").await;
 358        cx.shared_clipboard().await.assert_eq("jumps o");
 359        cx.set_shared_state(indoc! {"
 360            The quick brown
 361            fox jumps oveˇr
 362            the lazy dog"})
 363            .await;
 364        cx.simulate_shared_keystrokes("p").await;
 365        cx.shared_state().await.assert_eq(indoc! {"
 366            The quick brown
 367            fox jumps overjumps ˇo
 368            the lazy dog"});
 369
 370        cx.set_shared_state(indoc! {"
 371            The quick brown
 372            fox jumps oveˇr
 373            the lazy dog"})
 374            .await;
 375        cx.simulate_shared_keystrokes("shift-p").await;
 376        cx.shared_state().await.assert_eq(indoc! {"
 377            The quick brown
 378            fox jumps ovejumps ˇor
 379            the lazy dog"});
 380
 381        // line mode
 382        cx.set_shared_state(indoc! {"
 383            The quick brown
 384            fox juˇmps over
 385            the lazy dog"})
 386            .await;
 387        cx.simulate_shared_keystrokes("d d").await;
 388        cx.shared_clipboard().await.assert_eq("fox jumps over\n");
 389        cx.shared_state().await.assert_eq(indoc! {"
 390            The quick brown
 391            the laˇzy dog"});
 392        cx.simulate_shared_keystrokes("p").await;
 393        cx.shared_state().await.assert_eq(indoc! {"
 394            The quick brown
 395            the lazy dog
 396            ˇfox jumps over"});
 397        cx.simulate_shared_keystrokes("k shift-p").await;
 398        cx.shared_state().await.assert_eq(indoc! {"
 399            The quick brown
 400            ˇfox jumps over
 401            the lazy dog
 402            fox jumps over"});
 403
 404        // multiline, cursor to first character of pasted text.
 405        cx.set_shared_state(indoc! {"
 406            The quick brown
 407            fox jumps ˇover
 408            the lazy dog"})
 409            .await;
 410        cx.simulate_shared_keystrokes("v j y").await;
 411        cx.shared_clipboard().await.assert_eq("over\nthe lazy do");
 412
 413        cx.simulate_shared_keystrokes("p").await;
 414        cx.shared_state().await.assert_eq(indoc! {"
 415            The quick brown
 416            fox jumps oˇover
 417            the lazy dover
 418            the lazy dog"});
 419        cx.simulate_shared_keystrokes("u shift-p").await;
 420        cx.shared_state().await.assert_eq(indoc! {"
 421            The quick brown
 422            fox jumps ˇover
 423            the lazy doover
 424            the lazy dog"});
 425    }
 426
 427    #[gpui::test]
 428    async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) {
 429        let mut cx = VimTestContext::new(cx, true).await;
 430
 431        cx.update_global(|store: &mut SettingsStore, cx| {
 432            store.update_user_settings(cx, |s| {
 433                s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
 434            });
 435        });
 436
 437        cx.set_state(
 438            indoc! {"
 439                The quick brown
 440                fox jˇumps over
 441                the lazy dog"},
 442            Mode::Normal,
 443        );
 444        cx.simulate_keystrokes("v i w y");
 445        cx.assert_state(
 446            indoc! {"
 447                The quick brown
 448                fox ˇjumps over
 449                the lazy dog"},
 450            Mode::Normal,
 451        );
 452        cx.simulate_keystrokes("p");
 453        cx.assert_state(
 454            indoc! {"
 455                The quick brown
 456                fox jjumpˇsumps over
 457                the lazy dog"},
 458            Mode::Normal,
 459        );
 460        assert_eq!(cx.read_from_clipboard(), None);
 461    }
 462
 463    #[gpui::test]
 464    async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) {
 465        let mut cx = VimTestContext::new(cx, true).await;
 466
 467        cx.update_global(|store: &mut SettingsStore, cx| {
 468            store.update_user_settings(cx, |s| {
 469                s.vim.get_or_insert_default().use_system_clipboard =
 470                    Some(UseSystemClipboard::OnYank)
 471            });
 472        });
 473
 474        // copy in visual mode
 475        cx.set_state(
 476            indoc! {"
 477                The quick brown
 478                fox jˇumps over
 479                the lazy dog"},
 480            Mode::Normal,
 481        );
 482        cx.simulate_keystrokes("v i w y");
 483        cx.assert_state(
 484            indoc! {"
 485                The quick brown
 486                fox ˇjumps over
 487                the lazy dog"},
 488            Mode::Normal,
 489        );
 490        cx.simulate_keystrokes("p");
 491        cx.assert_state(
 492            indoc! {"
 493                The quick brown
 494                fox jjumpˇsumps over
 495                the lazy dog"},
 496            Mode::Normal,
 497        );
 498        assert_eq!(
 499            cx.read_from_clipboard().map(|item| item.text().unwrap()),
 500            Some("jumps".into())
 501        );
 502        cx.simulate_keystrokes("d d p");
 503        cx.assert_state(
 504            indoc! {"
 505                The quick brown
 506                the lazy dog
 507                ˇfox jjumpsumps over"},
 508            Mode::Normal,
 509        );
 510        assert_eq!(
 511            cx.read_from_clipboard().map(|item| item.text().unwrap()),
 512            Some("jumps".into())
 513        );
 514        cx.write_to_clipboard(ClipboardItem::new_string("test-copy".to_string()));
 515        cx.simulate_keystrokes("shift-p");
 516        cx.assert_state(
 517            indoc! {"
 518                The quick brown
 519                the lazy dog
 520                test-copˇyfox jjumpsumps over"},
 521            Mode::Normal,
 522        );
 523    }
 524
 525    #[gpui::test]
 526    async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
 527        let mut cx = NeovimBackedTestContext::new(cx).await;
 528
 529        // copy in visual mode
 530        cx.set_shared_state(indoc! {"
 531                The quick brown
 532                fox jˇumps over
 533                the lazy dog"})
 534            .await;
 535        cx.simulate_shared_keystrokes("v i w y").await;
 536        cx.shared_state().await.assert_eq(indoc! {"
 537                The quick brown
 538                fox ˇjumps over
 539                the lazy dog"});
 540        // paste in visual mode
 541        cx.simulate_shared_keystrokes("w v i w p").await;
 542        cx.shared_state().await.assert_eq(indoc! {"
 543                The quick brown
 544                fox jumps jumpˇs
 545                the lazy dog"});
 546        cx.shared_clipboard().await.assert_eq("over");
 547        // paste in visual line mode
 548        cx.simulate_shared_keystrokes("up shift-v shift-p").await;
 549        cx.shared_state().await.assert_eq(indoc! {"
 550            ˇover
 551            fox jumps jumps
 552            the lazy dog"});
 553        cx.shared_clipboard().await.assert_eq("over");
 554        // paste in visual block mode
 555        cx.simulate_shared_keystrokes("ctrl-v down down p").await;
 556        cx.shared_state().await.assert_eq(indoc! {"
 557            oveˇrver
 558            overox jumps jumps
 559            overhe lazy dog"});
 560
 561        // copy in visual line mode
 562        cx.set_shared_state(indoc! {"
 563                The quick brown
 564                fox juˇmps over
 565                the lazy dog"})
 566            .await;
 567        cx.simulate_shared_keystrokes("shift-v d").await;
 568        cx.shared_state().await.assert_eq(indoc! {"
 569                The quick brown
 570                the laˇzy dog"});
 571        // paste in visual mode
 572        cx.simulate_shared_keystrokes("v i w p").await;
 573        cx.shared_state().await.assert_eq(indoc! {"
 574                The quick brown
 575                the•
 576                ˇfox jumps over
 577                 dog"});
 578        cx.shared_clipboard().await.assert_eq("lazy");
 579        cx.set_shared_state(indoc! {"
 580            The quick brown
 581            fox juˇmps over
 582            the lazy dog"})
 583            .await;
 584        cx.simulate_shared_keystrokes("shift-v d").await;
 585        cx.shared_state().await.assert_eq(indoc! {"
 586            The quick brown
 587            the laˇzy dog"});
 588        cx.shared_clipboard().await.assert_eq("fox jumps over\n");
 589        // paste in visual line mode
 590        cx.simulate_shared_keystrokes("k shift-v p").await;
 591        cx.shared_state().await.assert_eq(indoc! {"
 592            ˇfox jumps over
 593            the lazy dog"});
 594        cx.shared_clipboard().await.assert_eq("The quick brown\n");
 595
 596        // Copy line and paste in visual mode, with cursor on newline character.
 597        cx.set_shared_state(indoc! {"
 598            ˇThe quick brown
 599            fox jumps over
 600            the lazy dog"})
 601            .await;
 602        cx.simulate_shared_keystrokes("y y shift-v j $ p").await;
 603        cx.shared_state().await.assert_eq(indoc! {"
 604            ˇThe quick brown
 605            the lazy dog"});
 606    }
 607
 608    #[gpui::test]
 609    async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
 610        let mut cx = NeovimBackedTestContext::new(cx).await;
 611        // copy in visual block mode
 612        cx.set_shared_state(indoc! {"
 613            The ˇquick brown
 614            fox jumps over
 615            the lazy dog"})
 616            .await;
 617        cx.simulate_shared_keystrokes("ctrl-v 2 j y").await;
 618        cx.shared_clipboard().await.assert_eq("q\nj\nl");
 619        cx.simulate_shared_keystrokes("p").await;
 620        cx.shared_state().await.assert_eq(indoc! {"
 621            The qˇquick brown
 622            fox jjumps over
 623            the llazy dog"});
 624        cx.simulate_shared_keystrokes("v i w shift-p").await;
 625        cx.shared_state().await.assert_eq(indoc! {"
 626            The ˇq brown
 627            fox jjjumps over
 628            the lllazy dog"});
 629        cx.simulate_shared_keystrokes("v i w shift-p").await;
 630
 631        cx.set_shared_state(indoc! {"
 632            The ˇquick brown
 633            fox jumps over
 634            the lazy dog"})
 635            .await;
 636        cx.simulate_shared_keystrokes("ctrl-v j y").await;
 637        cx.shared_clipboard().await.assert_eq("q\nj");
 638        cx.simulate_shared_keystrokes("l ctrl-v 2 j shift-p").await;
 639        cx.shared_state().await.assert_eq(indoc! {"
 640            The qˇqick brown
 641            fox jjmps over
 642            the lzy dog"});
 643
 644        cx.simulate_shared_keystrokes("shift-v p").await;
 645        cx.shared_state().await.assert_eq(indoc! {"
 646            ˇq
 647            j
 648            fox jjmps over
 649            the lzy dog"});
 650    }
 651
 652    #[gpui::test]
 653    async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
 654        let mut cx = VimTestContext::new_typescript(cx).await;
 655
 656        cx.set_state(
 657            indoc! {"
 658            class A {ˇ
 659            }
 660        "},
 661            Mode::Normal,
 662        );
 663        cx.simulate_keystrokes("o a ( ) { escape");
 664        cx.assert_state(
 665            indoc! {"
 666            class A {
 667                a()ˇ{}
 668            }
 669            "},
 670            Mode::Normal,
 671        );
 672        // cursor goes to the first non-blank character in the line;
 673        cx.simulate_keystrokes("y y p");
 674        cx.assert_state(
 675            indoc! {"
 676            class A {
 677                a(){}
 678                ˇa(){}
 679            }
 680            "},
 681            Mode::Normal,
 682        );
 683        // indentation is preserved when pasting
 684        cx.simulate_keystrokes("u shift-v up y shift-p");
 685        cx.assert_state(
 686            indoc! {"
 687                ˇclass A {
 688                    a(){}
 689                class A {
 690                    a(){}
 691                }
 692            "},
 693            Mode::Normal,
 694        );
 695    }
 696
 697    #[gpui::test]
 698    async fn test_paste_auto_indent(cx: &mut gpui::TestAppContext) {
 699        let mut cx = VimTestContext::new(cx, true).await;
 700
 701        cx.set_state(
 702            indoc! {"
 703            mod some_module {
 704                ˇfn main() {
 705                }
 706            }
 707            "},
 708            Mode::Normal,
 709        );
 710        // default auto indentation
 711        cx.simulate_keystrokes("y y p");
 712        cx.assert_state(
 713            indoc! {"
 714                mod some_module {
 715                    fn main() {
 716                        ˇfn main() {
 717                    }
 718                }
 719                "},
 720            Mode::Normal,
 721        );
 722        // back to previous state
 723        cx.simulate_keystrokes("u u");
 724        cx.assert_state(
 725            indoc! {"
 726                mod some_module {
 727                    ˇfn main() {
 728                    }
 729                }
 730                "},
 731            Mode::Normal,
 732        );
 733        cx.update_global(|store: &mut SettingsStore, cx| {
 734            store.update_user_settings(cx, |settings| {
 735                settings.project.all_languages.languages.0.insert(
 736                    LanguageName::new_static("Rust").0.to_string(),
 737                    LanguageSettingsContent {
 738                        auto_indent_on_paste: Some(false),
 739                        ..Default::default()
 740                    },
 741                );
 742            });
 743        });
 744        // auto indentation turned off
 745        cx.simulate_keystrokes("y y p");
 746        cx.assert_state(
 747            indoc! {"
 748                mod some_module {
 749                    fn main() {
 750                    ˇfn main() {
 751                    }
 752                }
 753                "},
 754            Mode::Normal,
 755        );
 756    }
 757
 758    #[gpui::test]
 759    async fn test_paste_count(cx: &mut gpui::TestAppContext) {
 760        let mut cx = NeovimBackedTestContext::new(cx).await;
 761
 762        cx.set_shared_state(indoc! {"
 763            onˇe
 764            two
 765            three
 766        "})
 767            .await;
 768        cx.simulate_shared_keystrokes("y y 3 p").await;
 769        cx.shared_state().await.assert_eq(indoc! {"
 770            one
 771            ˇone
 772            one
 773            one
 774            two
 775            three
 776        "});
 777
 778        cx.set_shared_state(indoc! {"
 779            one
 780            ˇtwo
 781            three
 782        "})
 783            .await;
 784        cx.simulate_shared_keystrokes("y $ $ 3 p").await;
 785        cx.shared_state().await.assert_eq(indoc! {"
 786            one
 787            twotwotwotwˇo
 788            three
 789        "});
 790    }
 791
 792    #[gpui::test]
 793    async fn test_paste_system_clipboard_never(cx: &mut gpui::TestAppContext) {
 794        let mut cx = VimTestContext::new(cx, true).await;
 795
 796        cx.update_global(|store: &mut SettingsStore, cx| {
 797            store.update_user_settings(cx, |s| {
 798                s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
 799            });
 800        });
 801
 802        cx.set_state(
 803            indoc! {"
 804                ˇThe quick brown
 805                fox jumps over
 806                the lazy dog"},
 807            Mode::Normal,
 808        );
 809
 810        cx.write_to_clipboard(ClipboardItem::new_string("something else".to_string()));
 811
 812        cx.simulate_keystrokes("d d");
 813        cx.assert_state(
 814            indoc! {"
 815                ˇfox jumps over
 816                the lazy dog"},
 817            Mode::Normal,
 818        );
 819
 820        cx.simulate_keystrokes("shift-v p");
 821        cx.assert_state(
 822            indoc! {"
 823                ˇThe quick brown
 824                the lazy dog"},
 825            Mode::Normal,
 826        );
 827
 828        cx.simulate_keystrokes("shift-v");
 829        cx.dispatch_action(editor::actions::Paste);
 830        cx.assert_state(
 831            indoc! {"
 832                ˇsomething else
 833                the lazy dog"},
 834            Mode::Normal,
 835        );
 836    }
 837
 838    #[gpui::test]
 839    async fn test_numbered_registers(cx: &mut gpui::TestAppContext) {
 840        let mut cx = NeovimBackedTestContext::new(cx).await;
 841
 842        cx.update_global(|store: &mut SettingsStore, cx| {
 843            store.update_user_settings(cx, |s| {
 844                s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
 845            });
 846        });
 847
 848        cx.set_shared_state(indoc! {"
 849                The quick brown
 850                fox jˇumps over
 851                the lazy dog"})
 852            .await;
 853        cx.simulate_shared_keystrokes("y y \" 0 p").await;
 854        cx.shared_register('0').await.assert_eq("fox jumps over\n");
 855        cx.shared_register('"').await.assert_eq("fox jumps over\n");
 856
 857        cx.shared_state().await.assert_eq(indoc! {"
 858                The quick brown
 859                fox jumps over
 860                ˇfox jumps over
 861                the lazy dog"});
 862        cx.simulate_shared_keystrokes("k k d d").await;
 863        cx.shared_register('0').await.assert_eq("fox jumps over\n");
 864        cx.shared_register('1').await.assert_eq("The quick brown\n");
 865        cx.shared_register('"').await.assert_eq("The quick brown\n");
 866
 867        cx.simulate_shared_keystrokes("d d shift-g d d").await;
 868        cx.shared_register('0').await.assert_eq("fox jumps over\n");
 869        cx.shared_register('3').await.assert_eq("The quick brown\n");
 870        cx.shared_register('2').await.assert_eq("fox jumps over\n");
 871        cx.shared_register('1').await.assert_eq("the lazy dog\n");
 872
 873        cx.shared_state().await.assert_eq(indoc! {"
 874        ˇfox jumps over"});
 875
 876        cx.simulate_shared_keystrokes("d d \" 3 p p \" 1 p").await;
 877        cx.set_shared_state(indoc! {"
 878                The quick brown
 879                fox jumps over
 880                ˇthe lazy dog"})
 881            .await;
 882    }
 883
 884    #[gpui::test]
 885    async fn test_named_registers(cx: &mut gpui::TestAppContext) {
 886        let mut cx = NeovimBackedTestContext::new(cx).await;
 887
 888        cx.update_global(|store: &mut SettingsStore, cx| {
 889            store.update_user_settings(cx, |s| {
 890                s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
 891            });
 892        });
 893
 894        cx.set_shared_state(indoc! {"
 895                The quick brown
 896                fox jˇumps over
 897                the lazy dog"})
 898            .await;
 899        cx.simulate_shared_keystrokes("\" a d a w").await;
 900        cx.shared_register('a').await.assert_eq("jumps ");
 901        cx.simulate_shared_keystrokes("\" shift-a d i w").await;
 902        cx.shared_register('a').await.assert_eq("jumps over");
 903        cx.shared_register('"').await.assert_eq("jumps over");
 904        cx.simulate_shared_keystrokes("\" a p").await;
 905        cx.shared_state().await.assert_eq(indoc! {"
 906                The quick brown
 907                fox jumps oveˇr
 908                the lazy dog"});
 909        cx.simulate_shared_keystrokes("\" a d a w").await;
 910        cx.shared_register('a').await.assert_eq(" over");
 911    }
 912
 913    #[gpui::test]
 914    async fn test_special_registers(cx: &mut gpui::TestAppContext) {
 915        let mut cx = NeovimBackedTestContext::new(cx).await;
 916
 917        cx.update_global(|store: &mut SettingsStore, cx| {
 918            store.update_user_settings(cx, |s| {
 919                s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
 920            });
 921        });
 922
 923        cx.set_shared_state(indoc! {"
 924                The quick brown
 925                fox jˇumps over
 926                the lazy dog"})
 927            .await;
 928        cx.simulate_shared_keystrokes("d i w").await;
 929        cx.shared_register('-').await.assert_eq("jumps");
 930        cx.simulate_shared_keystrokes("\" _ d d").await;
 931        cx.shared_register('_').await.assert_eq("");
 932
 933        cx.simulate_shared_keystrokes("shift-v \" _ y w").await;
 934        cx.shared_register('"').await.assert_eq("jumps");
 935
 936        cx.shared_state().await.assert_eq(indoc! {"
 937                The quick brown
 938                the ˇlazy dog"});
 939        cx.simulate_shared_keystrokes("\" \" d ^").await;
 940        cx.shared_register('0').await.assert_eq("the ");
 941        cx.shared_register('"').await.assert_eq("the ");
 942
 943        cx.simulate_shared_keystrokes("^ \" + d $").await;
 944        cx.shared_clipboard().await.assert_eq("lazy dog");
 945        cx.shared_register('"').await.assert_eq("lazy dog");
 946
 947        cx.simulate_shared_keystrokes("/ d o g enter").await;
 948        cx.shared_register('/').await.assert_eq("dog");
 949        cx.simulate_shared_keystrokes("\" / shift-p").await;
 950        cx.shared_state().await.assert_eq(indoc! {"
 951                The quick brown
 952                doˇg"});
 953
 954        // not testing nvim as it doesn't have a filename
 955        cx.simulate_keystrokes("\" % p");
 956        #[cfg(not(target_os = "windows"))]
 957        cx.assert_state(
 958            indoc! {"
 959                    The quick brown
 960                    dogdir/file.rˇs"},
 961            Mode::Normal,
 962        );
 963        #[cfg(target_os = "windows")]
 964        cx.assert_state(
 965            indoc! {"
 966                    The quick brown
 967                    dogdir\\file.rˇs"},
 968            Mode::Normal,
 969        );
 970    }
 971
 972    #[gpui::test]
 973    async fn test_multicursor_paste(cx: &mut gpui::TestAppContext) {
 974        let mut cx = VimTestContext::new(cx, true).await;
 975
 976        cx.update_global(|store: &mut SettingsStore, cx| {
 977            store.update_user_settings(cx, |s| {
 978                s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
 979            });
 980        });
 981
 982        cx.set_state(
 983            indoc! {"
 984               ˇfish one
 985               fish two
 986               fish red
 987               fish blue
 988                "},
 989            Mode::Normal,
 990        );
 991        cx.simulate_keystrokes("4 g l w escape d i w 0 shift-p");
 992        cx.assert_state(
 993            indoc! {"
 994               onˇefish•
 995               twˇofish•
 996               reˇdfish•
 997               bluˇefish•
 998                "},
 999            Mode::Normal,
1000        );
1001    }
1002
1003    #[gpui::test]
1004    async fn test_replace_with_register(cx: &mut gpui::TestAppContext) {
1005        let mut cx = VimTestContext::new(cx, true).await;
1006
1007        cx.set_state(
1008            indoc! {"
1009                   ˇfish one
1010                   two three
1011                   "},
1012            Mode::Normal,
1013        );
1014        cx.simulate_keystrokes("y i w");
1015        cx.simulate_keystrokes("w");
1016        cx.simulate_keystrokes("g shift-r i w");
1017        cx.assert_state(
1018            indoc! {"
1019                fish fisˇh
1020                two three
1021                "},
1022            Mode::Normal,
1023        );
1024        cx.simulate_keystrokes("j b g shift-r e");
1025        cx.assert_state(
1026            indoc! {"
1027            fish fish
1028            two fisˇh
1029            "},
1030            Mode::Normal,
1031        );
1032        let clipboard: Register = cx.read_from_clipboard().unwrap().into();
1033        assert_eq!(clipboard.text, "fish");
1034
1035        cx.set_state(
1036            indoc! {"
1037                   ˇfish one
1038                   two three
1039                   "},
1040            Mode::Normal,
1041        );
1042        cx.simulate_keystrokes("y i w");
1043        cx.simulate_keystrokes("w");
1044        cx.simulate_keystrokes("v i w g shift-r");
1045        cx.assert_state(
1046            indoc! {"
1047                fish fisˇh
1048                two three
1049                "},
1050            Mode::Normal,
1051        );
1052        cx.simulate_keystrokes("g shift-r r");
1053        cx.assert_state(
1054            indoc! {"
1055                fisˇh
1056                two three
1057                "},
1058            Mode::Normal,
1059        );
1060        cx.simulate_keystrokes("j w g shift-r $");
1061        cx.assert_state(
1062            indoc! {"
1063                fish
1064                two fisˇh
1065            "},
1066            Mode::Normal,
1067        );
1068        let clipboard: Register = cx.read_from_clipboard().unwrap().into();
1069        assert_eq!(clipboard.text, "fish");
1070    }
1071
1072    #[gpui::test]
1073    async fn test_replace_with_register_dot_repeat(cx: &mut gpui::TestAppContext) {
1074        let mut cx = VimTestContext::new(cx, true).await;
1075
1076        cx.set_state(
1077            indoc! {"
1078                   ˇfish one
1079                   two three
1080                   "},
1081            Mode::Normal,
1082        );
1083        cx.simulate_keystrokes("y i w");
1084        cx.simulate_keystrokes("w");
1085        cx.simulate_keystrokes("g shift-r i w");
1086        cx.assert_state(
1087            indoc! {"
1088                fish fisˇh
1089                two three
1090                "},
1091            Mode::Normal,
1092        );
1093        cx.simulate_keystrokes("j .");
1094        cx.assert_state(
1095            indoc! {"
1096                fish fish
1097                two fisˇh
1098                "},
1099            Mode::Normal,
1100        );
1101    }
1102
1103    #[gpui::test]
1104    async fn test_paste_entire_line_from_editor_copy(cx: &mut gpui::TestAppContext) {
1105        let mut cx = VimTestContext::new(cx, true).await;
1106
1107        cx.set_state(
1108            indoc! {"
1109                ˇline one
1110                line two
1111                line three"},
1112            Mode::Normal,
1113        );
1114
1115        // Simulate what the editor's do_copy produces for two entire-line selections:
1116        // entire-line selections are NOT separated by an extra newline in the clipboard text.
1117        let clipboard_text = "line one\nline two\n".to_string();
1118        let clipboard_selections = vec![
1119            editor::ClipboardSelection {
1120                len: "line one\n".len(),
1121                is_entire_line: true,
1122                first_line_indent: 0,
1123                file_path: None,
1124                line_range: None,
1125            },
1126            editor::ClipboardSelection {
1127                len: "line two\n".len(),
1128                is_entire_line: true,
1129                first_line_indent: 0,
1130                file_path: None,
1131                line_range: None,
1132            },
1133        ];
1134        cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
1135            clipboard_text,
1136            clipboard_selections,
1137        ));
1138
1139        cx.simulate_keystrokes("p");
1140        cx.assert_state(
1141            indoc! {"
1142                line one
1143                ˇline one
1144                line two
1145                line two
1146                line three"},
1147            Mode::Normal,
1148        );
1149    }
1150}