use anyhow::Result; use std::sync::Arc; use collections::HashMap; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, movement, scroll::autoscroll::Autoscroll, Bias, DisplayPoint, Editor, }; use gpui::{actions, AppContext, ViewContext, WindowContext}; use language::{Selection, SelectionGoal}; use workspace::Workspace; use crate::{ motion::{start_of_line, Motion}, object::Object, state::{Mode, Operator}, utils::copy_selections_content, Vim, }; actions!( vim, [ ToggleVisual, ToggleVisualLine, ToggleVisualBlock, VisualDelete, VisualYank, OtherEnd, SelectNext, SelectPrevious, ] ); pub fn init(cx: &mut AppContext) { cx.add_action(|_, _: &ToggleVisual, cx: &mut ViewContext| { toggle_mode(Mode::Visual, cx) }); cx.add_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext| { toggle_mode(Mode::VisualLine, cx) }); cx.add_action( |_, _: &ToggleVisualBlock, cx: &mut ViewContext| { toggle_mode(Mode::VisualBlock, cx) }, ); cx.add_action(other_end); cx.add_action(delete); cx.add_action(yank); cx.add_action(select_next); cx.add_action(select_previous); } pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { let text_layout_details = editor.text_layout_details(cx); if vim.state().mode == Mode::VisualBlock && !matches!( motion, Motion::EndOfLine { display_lines: false } ) { let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. }); visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| { motion.move_point(map, point, goal, times, &text_layout_details) }) } else { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { let was_reversed = selection.reversed; let mut current_head = selection.head(); // our motions assume the current character is after the cursor, // but in (forward) visual mode the current character is just // before the end of the selection. // If the file ends with a newline (which is common) we don't do this. // so that if you go to the end of such a file you can use "up" to go // to the previous line and have it work somewhat as expected. if !selection.reversed && !selection.is_empty() && !(selection.end.column() == 0 && selection.end == map.max_point()) { current_head = movement::left(map, selection.end) } let Some((new_head, goal)) = motion.move_point( map, current_head, selection.goal, times, &text_layout_details, ) else { return; }; selection.set_head(new_head, goal); // ensure the current character is included in the selection. if !selection.reversed { let next_point = if vim.state().mode == Mode::VisualBlock { movement::saturating_right(map, selection.end) } else { movement::right(map, selection.end) }; if !(next_point.column() == 0 && next_point == map.max_point()) { selection.end = next_point; } } // vim always ensures the anchor character stays selected. // if our selection has reversed, we need to move the opposite end // to ensure the anchor is still selected. if was_reversed && !selection.reversed { selection.start = movement::left(map, selection.start); } else if !was_reversed && selection.reversed { selection.end = movement::right(map, selection.end); } }) }); } }); }); } pub fn visual_block_motion( preserve_goal: bool, editor: &mut Editor, cx: &mut ViewContext, mut move_selection: impl FnMut( &DisplaySnapshot, DisplayPoint, SelectionGoal, ) -> Option<(DisplayPoint, SelectionGoal)>, ) { let text_layout_details = editor.text_layout_details(cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { let map = &s.display_map(); let mut head = s.newest_anchor().head().to_display_point(map); let mut tail = s.oldest_anchor().tail().to_display_point(map); let mut head_x = map.x_for_point(head, &text_layout_details); let mut tail_x = map.x_for_point(tail, &text_layout_details); let (start, end) = match s.newest_anchor().goal { SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end), SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start), _ => (tail_x, head_x), }; let mut goal = SelectionGoal::HorizontalRange { start, end }; let was_reversed = tail_x > head_x; if !was_reversed && !preserve_goal { head = movement::saturating_left(map, head); } let Some((new_head, _)) = move_selection(&map, head, goal) else { return; }; head = new_head; head_x = map.x_for_point(head, &text_layout_details); let is_reversed = tail_x > head_x; if was_reversed && !is_reversed { tail = movement::saturating_left(map, tail); tail_x = map.x_for_point(tail, &text_layout_details); } else if !was_reversed && is_reversed { tail = movement::saturating_right(map, tail); tail_x = map.x_for_point(tail, &text_layout_details); } if !is_reversed && !preserve_goal { head = movement::saturating_right(map, head); head_x = map.x_for_point(head, &text_layout_details); } let positions = if is_reversed { head_x..tail_x } else { tail_x..head_x }; if !preserve_goal { goal = SelectionGoal::HorizontalRange { start: positions.start, end: positions.end, }; } let mut selections = Vec::new(); let mut row = tail.row(); loop { let layed_out_line = map.lay_out_line_for_row(row, &text_layout_details); let start = DisplayPoint::new( row, layed_out_line.closest_index_for_x(positions.start) as u32, ); let mut end = DisplayPoint::new( row, layed_out_line.closest_index_for_x(positions.end) as u32, ); if end <= start { if start.column() == map.line_len(start.row()) { end = start; } else { end = movement::saturating_right(map, start); } } if positions.start <= layed_out_line.width() { let selection = Selection { id: s.new_selection_id(), start: start.to_point(map), end: end.to_point(map), reversed: is_reversed, goal: goal.clone(), }; selections.push(selection); } if row == head.row() { break; } if tail.row() > head.row() { row -= 1 } else { row += 1 } } s.select(selections); }) } pub fn visual_object(object: Object, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { if let Some(Operator::Object { around }) = vim.active_operator() { vim.pop_operator(cx); let current_mode = vim.state().mode; let target_mode = object.target_visual_mode(current_mode); if target_mode != current_mode { vim.switch_mode(target_mode, true, cx); } vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { let mut head = selection.head(); // all our motions assume that the current character is // after the cursor; however in the case of a visual selection // the current character is before the cursor. if !selection.reversed { head = movement::left(map, head); } if let Some(range) = object.range(map, head, around) { if !range.is_empty() { let expand_both_ways = object.always_expands_both_ways() || selection.is_empty() || movement::right(map, selection.start) == selection.end; if expand_both_ways { selection.start = range.start; selection.end = range.end; } else if selection.reversed { selection.start = range.start; } else { selection.end = range.end; } } } }); }); }); } }); } fn toggle_mode(mode: Mode, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { if vim.state().mode == mode { vim.switch_mode(Mode::Normal, false, cx); } else { vim.switch_mode(mode, false, cx); } }) } pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.change_selections(None, cx, |s| { s.move_with(|_, selection| { selection.reversed = !selection.reversed; }) }) }) }); } pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); vim.update_active_editor(cx, |editor, cx| { let mut original_columns: HashMap<_, _> = Default::default(); let line_mode = editor.selections.line_mode; editor.transact(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { if line_mode { let mut position = selection.head(); if !selection.reversed { position = movement::left(map, position); } original_columns.insert(selection.id, position.to_point(map).column); } selection.goal = SelectionGoal::None; }); }); copy_selections_content(editor, line_mode, cx); editor.insert("", cx); // Fixup cursor position after the deletion editor.set_clip_at_line_ends(true, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { let mut cursor = selection.head().to_point(map); if let Some(column) = original_columns.get(&selection.id) { cursor.column = *column } let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left); selection.collapse_to(cursor, selection.goal) }); if vim.state().mode == Mode::VisualBlock { s.select_anchors(vec![s.first_anchor()]) } }); }) }); vim.switch_mode(Mode::Normal, true, cx); }); } pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { let line_mode = editor.selections.line_mode; copy_selections_content(editor, line_mode, cx); editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { if line_mode { selection.start = start_of_line(map, false, selection.start); }; selection.collapse_to(selection.start, SelectionGoal::None) }); if vim.state().mode == Mode::VisualBlock { s.select_anchors(vec![s.first_anchor()]) } }); }); vim.switch_mode(Mode::Normal, true, cx); }); } pub(crate) fn visual_replace(text: Arc, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.stop_recording(); vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { let (display_map, selections) = editor.selections.all_adjusted_display(cx); // Selections are biased right at the start. So we need to store // anchors that are biased left so that we can restore the selections // after the change let stable_anchors = editor .selections .disjoint_anchors() .into_iter() .map(|selection| { let start = selection.start.bias_left(&display_map.buffer_snapshot); start..start }) .collect::>(); let mut edits = Vec::new(); for selection in selections.iter() { let selection = selection.clone(); for row_range in movement::split_display_range_by_lines(&display_map, selection.range()) { let range = row_range.start.to_offset(&display_map, Bias::Right) ..row_range.end.to_offset(&display_map, Bias::Right); let text = text.repeat(range.len()); edits.push((range, text)); } } editor.buffer().update(cx, |buffer, cx| { buffer.edit(edits, None, cx); }); editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors)); }); }); vim.switch_mode(Mode::Normal, false, cx); }); } pub fn select_next( _: &mut Workspace, _: &SelectNext, cx: &mut ViewContext, ) -> Result<()> { Vim::update(cx, |vim, cx| { let count = vim.take_count(cx) .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); vim.update_active_editor(cx, |editor, cx| { for _ in 0..count { match editor.select_next(&Default::default(), cx) { Err(a) => return Err(a), _ => {} } } Ok(()) }) }) .unwrap_or(Ok(())) } pub fn select_previous( _: &mut Workspace, _: &SelectPrevious, cx: &mut ViewContext, ) -> Result<()> { Vim::update(cx, |vim, cx| { let count = vim.take_count(cx) .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); vim.update_active_editor(cx, |editor, cx| { for _ in 0..count { match editor.select_previous(&Default::default(), cx) { Err(a) => return Err(a), _ => {} } } Ok(()) }) }) .unwrap_or(Ok(())) } #[cfg(test)] mod test { use indoc::indoc; use workspace::item::Item; use crate::{ state::Mode, test::{NeovimBackedTestContext, VimTestContext}, }; #[gpui::test] async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.set_shared_state(indoc! { "The ˇquick brown fox jumps over the lazy dog" }) .await; let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx)); // entering visual mode should select the character // under cursor cx.simulate_shared_keystrokes(["v"]).await; cx.assert_shared_state(indoc! { "The «qˇ»uick brown fox jumps over the lazy dog"}) .await; cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); // forwards motions should extend the selection cx.simulate_shared_keystrokes(["w", "j"]).await; cx.assert_shared_state(indoc! { "The «quick brown fox jumps oˇ»ver the lazy dog"}) .await; cx.simulate_shared_keystrokes(["escape"]).await; assert_eq!(Mode::Normal, cx.neovim_mode().await); cx.assert_shared_state(indoc! { "The quick brown fox jumps ˇover the lazy dog"}) .await; // motions work backwards cx.simulate_shared_keystrokes(["v", "k", "b"]).await; cx.assert_shared_state(indoc! { "The «ˇquick brown fox jumps o»ver the lazy dog"}) .await; // works on empty lines cx.set_shared_state(indoc! {" a ˇ b "}) .await; let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx)); cx.simulate_shared_keystrokes(["v"]).await; cx.assert_shared_state(indoc! {" a « ˇ»b "}) .await; cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); // toggles off again cx.simulate_shared_keystrokes(["v"]).await; cx.assert_shared_state(indoc! {" a ˇ b "}) .await; // works at the end of a document cx.set_shared_state(indoc! {" a b ˇ"}) .await; cx.simulate_shared_keystrokes(["v"]).await; cx.assert_shared_state(indoc! {" a b ˇ"}) .await; assert_eq!(cx.mode(), cx.neovim_mode().await); } #[gpui::test] async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.set_shared_state(indoc! { "The ˇquick brown fox jumps over the lazy dog" }) .await; cx.simulate_shared_keystrokes(["shift-v"]).await; cx.assert_shared_state(indoc! { "The «qˇ»uick brown fox jumps over the lazy dog"}) .await; assert_eq!(cx.mode(), cx.neovim_mode().await); cx.simulate_shared_keystrokes(["x"]).await; cx.assert_shared_state(indoc! { "fox ˇjumps over the lazy dog"}) .await; // it should work on empty lines cx.set_shared_state(indoc! {" a ˇ b"}) .await; cx.simulate_shared_keystrokes(["shift-v"]).await; cx.assert_shared_state(indoc! { " a « ˇ»b"}) .await; cx.simulate_shared_keystrokes(["x"]).await; cx.assert_shared_state(indoc! { " a ˇb"}) .await; // it should work at the end of the document cx.set_shared_state(indoc! {" a b ˇ"}) .await; let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx)); cx.simulate_shared_keystrokes(["shift-v"]).await; cx.assert_shared_state(indoc! {" a b ˇ"}) .await; assert_eq!(cx.mode(), cx.neovim_mode().await); cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); cx.simulate_shared_keystrokes(["x"]).await; cx.assert_shared_state(indoc! {" a ˇb"}) .await; } #[gpui::test] async fn test_visual_delete(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.assert_binding_matches(["v", "w"], "The quick ˇbrown") .await; cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown") .await; cx.assert_binding_matches( ["v", "w", "j", "x"], indoc! {" The ˇquick brown fox jumps over the lazy dog"}, ) .await; // Test pasting code copied on delete cx.simulate_shared_keystrokes(["j", "p"]).await; cx.assert_state_matches().await; let mut cx = cx.binding(["v", "w", "j", "x"]); cx.assert_all(indoc! {" The ˇquick brown fox jumps over the ˇlazy dog"}) .await; let mut cx = cx.binding(["v", "b", "k", "x"]); cx.assert_all(indoc! {" The ˇquick brown fox jumps ˇover the ˇlazy dog"}) .await; } #[gpui::test] async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.set_shared_state(indoc! {" The quˇick brown fox jumps over the lazy dog"}) .await; cx.simulate_shared_keystrokes(["shift-v", "x"]).await; cx.assert_state_matches().await; // Test pasting code copied on delete cx.simulate_shared_keystroke("p").await; cx.assert_state_matches().await; cx.set_shared_state(indoc! {" The quick brown fox jumps over the laˇzy dog"}) .await; cx.simulate_shared_keystrokes(["shift-v", "x"]).await; cx.assert_state_matches().await; cx.assert_shared_clipboard("the lazy dog\n").await; for marked_text in cx.each_marked_position(indoc! {" The quˇick brown fox jumps over the lazy dog"}) { cx.set_shared_state(&marked_text).await; cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await; cx.assert_state_matches().await; // Test pasting code copied on delete cx.simulate_shared_keystroke("p").await; cx.assert_state_matches().await; } cx.set_shared_state(indoc! {" The ˇlong line should not crash "}) .await; cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await; cx.assert_state_matches().await; } #[gpui::test] async fn test_visual_yank(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.set_shared_state("The quick ˇbrown").await; cx.simulate_shared_keystrokes(["v", "w", "y"]).await; cx.assert_shared_state("The quick ˇbrown").await; cx.assert_shared_clipboard("brown").await; cx.set_shared_state(indoc! {" The ˇquick brown fox jumps over the lazy dog"}) .await; cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await; cx.assert_shared_state(indoc! {" The ˇquick brown fox jumps over the lazy dog"}) .await; cx.assert_shared_clipboard(indoc! {" quick brown fox jumps o"}) .await; cx.set_shared_state(indoc! {" The quick brown fox jumps over the ˇlazy dog"}) .await; cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await; cx.assert_shared_state(indoc! {" The quick brown fox jumps over the ˇlazy dog"}) .await; cx.assert_shared_clipboard("lazy d").await; cx.simulate_shared_keystrokes(["shift-v", "y"]).await; cx.assert_shared_clipboard("the lazy dog\n").await; let mut cx = cx.binding(["v", "b", "k", "y"]); cx.set_shared_state(indoc! {" The ˇquick brown fox jumps over the lazy dog"}) .await; cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await; cx.assert_shared_state(indoc! {" ˇThe quick brown fox jumps over the lazy dog"}) .await; cx.assert_clipboard_content(Some("The q")); cx.set_shared_state(indoc! {" The quick brown fox ˇjumps over the lazy dog"}) .await; cx.simulate_shared_keystrokes(["shift-v", "shift-g", "shift-y"]) .await; cx.assert_shared_state(indoc! {" The quick brown ˇfox jumps over the lazy dog"}) .await; cx.assert_shared_clipboard("fox jumps over\nthe lazy dog\n") .await; } #[gpui::test] async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.set_shared_state(indoc! { "The ˇquick brown fox jumps over the lazy dog" }) .await; cx.simulate_shared_keystrokes(["ctrl-v"]).await; cx.assert_shared_state(indoc! { "The «qˇ»uick brown fox jumps over the lazy dog" }) .await; cx.simulate_shared_keystrokes(["2", "down"]).await; cx.assert_shared_state(indoc! { "The «qˇ»uick brown fox «jˇ»umps over the «lˇ»azy dog" }) .await; cx.simulate_shared_keystrokes(["e"]).await; cx.assert_shared_state(indoc! { "The «quicˇ»k brown fox «jumpˇ»s over the «lazyˇ» dog" }) .await; cx.simulate_shared_keystrokes(["^"]).await; cx.assert_shared_state(indoc! { "«ˇThe q»uick brown «ˇfox j»umps over «ˇthe l»azy dog" }) .await; cx.simulate_shared_keystrokes(["$"]).await; cx.assert_shared_state(indoc! { "The «quick brownˇ» fox «jumps overˇ» the «lazy dogˇ»" }) .await; cx.simulate_shared_keystrokes(["shift-f", " "]).await; cx.assert_shared_state(indoc! { "The «quickˇ» brown fox «jumpsˇ» over the «lazy ˇ»dog" }) .await; // toggling through visual mode works as expected cx.simulate_shared_keystrokes(["v"]).await; cx.assert_shared_state(indoc! { "The «quick brown fox jumps over the lazy ˇ»dog" }) .await; cx.simulate_shared_keystrokes(["ctrl-v"]).await; cx.assert_shared_state(indoc! { "The «quickˇ» brown fox «jumpsˇ» over the «lazy ˇ»dog" }) .await; cx.set_shared_state(indoc! { "The ˇquick brown fox jumps over the lazy dog " }) .await; cx.simulate_shared_keystrokes(["ctrl-v", "down", "down"]) .await; cx.assert_shared_state(indoc! { "The«ˇ q»uick bro«ˇwn» foxˇ jumps over the lazy dog " }) .await; cx.simulate_shared_keystrokes(["down"]).await; cx.assert_shared_state(indoc! { "The «qˇ»uick brow«nˇ» fox jump«sˇ» over the lazy dog " }) .await; cx.simulate_shared_keystroke("left").await; cx.assert_shared_state(indoc! { "The«ˇ q»uick bro«ˇwn» foxˇ jum«ˇps» over the lazy dog " }) .await; cx.simulate_shared_keystrokes(["s", "o", "escape"]).await; cx.assert_shared_state(indoc! { "Theˇouick broo foxo jumo over the lazy dog " }) .await; //https://github.com/zed-industries/community/issues/1950 cx.set_shared_state(indoc! { "Theˇ quick brown fox jumps over the lazy dog " }) .await; cx.simulate_shared_keystrokes(["l", "ctrl-v", "j", "j"]) .await; cx.assert_shared_state(indoc! { "The «qˇ»uick brown fox «jˇ»umps over the lazy dog " }) .await; } #[gpui::test] async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.set_shared_state(indoc! { "The ˇquick brown fox jumps over the lazy dog " }) .await; cx.simulate_shared_keystrokes(["ctrl-v", "right", "down"]) .await; cx.assert_shared_state(indoc! { "The «quˇ»ick brown fox «juˇ»mps over the lazy dog " }) .await; } #[gpui::test] async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.set_shared_state(indoc! { "ˇThe quick brown fox jumps over the lazy dog " }) .await; cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await; cx.assert_shared_state(indoc! { "«Tˇ»he quick brown «fˇ»ox jumps over «tˇ»he lazy dog ˇ" }) .await; cx.simulate_shared_keystrokes(["shift-i", "k", "escape"]) .await; cx.assert_shared_state(indoc! { "ˇkThe quick brown kfox jumps over kthe lazy dog k" }) .await; cx.set_shared_state(indoc! { "ˇThe quick brown fox jumps over the lazy dog " }) .await; cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await; cx.assert_shared_state(indoc! { "«Tˇ»he quick brown «fˇ»ox jumps over «tˇ»he lazy dog ˇ" }) .await; cx.simulate_shared_keystrokes(["c", "k", "escape"]).await; cx.assert_shared_state(indoc! { "ˇkhe quick brown kox jumps over khe lazy dog k" }) .await; } #[gpui::test] async fn test_visual_object(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.set_shared_state("hello (in [parˇens] o)").await; cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await; cx.simulate_shared_keystrokes(["a", "]"]).await; cx.assert_shared_state("hello (in «[parens]ˇ» o)").await; assert_eq!(cx.mode(), Mode::Visual); cx.simulate_shared_keystrokes(["i", "("]).await; cx.assert_shared_state("hello («in [parens] oˇ»)").await; cx.set_shared_state("hello in a wˇord again.").await; cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"]) .await; cx.assert_shared_state("hello in a w«ordˇ» again.").await; assert_eq!(cx.mode(), Mode::VisualBlock); cx.simulate_shared_keystrokes(["o", "a", "s"]).await; cx.assert_shared_state("«ˇhello in a word» again.").await; assert_eq!(cx.mode(), Mode::Visual); } #[gpui::test] async fn test_mode_across_command(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; cx.set_state("aˇbc", Mode::Normal); cx.simulate_keystrokes(["ctrl-v"]); assert_eq!(cx.mode(), Mode::VisualBlock); cx.simulate_keystrokes(["cmd-shift-p", "escape"]); assert_eq!(cx.mode(), Mode::VisualBlock); } }