use crate::{ motion::{self, Motion}, object::Object, state::Mode, Vim, }; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, movement::TextLayoutDetails, scroll::Autoscroll, Bias, DisplayPoint, }; use gpui::{Context, Window}; use language::Selection; impl Vim { pub fn change_motion( &mut self, motion: Motion, times: Option, window: &mut Window, cx: &mut Context, ) { // Some motions ignore failure when switching to normal mode let mut motion_succeeded = matches!( motion, Motion::Left | Motion::Right | Motion::EndOfLine { .. } | Motion::WrappingLeft | Motion::StartOfLine { .. } ); self.update_editor(window, cx, |vim, editor, window, cx| { let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now editor.set_clip_at_line_ends(false, cx); editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_with(|map, selection| { motion_succeeded |= match motion { Motion::NextWordStart { ignore_punctuation } | Motion::NextSubwordStart { ignore_punctuation } => { expand_changed_word_selection( map, selection, times, ignore_punctuation, &text_layout_details, motion == Motion::NextSubwordStart { ignore_punctuation }, ) } _ => { let result = motion.expand_selection( map, selection, times, false, &text_layout_details, ); if let Motion::CurrentLine = motion { let mut start_offset = selection.start.to_offset(map, Bias::Left); let classifier = map .buffer_snapshot .char_classifier_at(selection.start.to_point(map)); for (ch, offset) in map.buffer_chars_at(start_offset) { if ch == '\n' || !classifier.is_whitespace(ch) { break; } start_offset = offset + ch.len_utf8(); } selection.start = start_offset.to_display_point(map); } result } } }); }); vim.copy_selections_content(editor, motion.linewise(), cx); editor.insert("", window, cx); editor.refresh_inline_completion(true, false, window, cx); }); }); if motion_succeeded { self.switch_mode(Mode::Insert, false, window, cx) } else { self.switch_mode(Mode::Normal, false, window, cx) } } pub fn change_object( &mut self, object: Object, around: bool, window: &mut Window, cx: &mut Context, ) { let mut objects_found = false; self.update_editor(window, cx, |vim, editor, window, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now editor.set_clip_at_line_ends(false, cx); editor.transact(window, cx, |editor, window, cx| { editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_with(|map, selection| { objects_found |= object.expand_selection(map, selection, around); }); }); if objects_found { vim.copy_selections_content(editor, false, cx); editor.insert("", window, cx); editor.refresh_inline_completion(true, false, window, cx); } }); }); if objects_found { self.switch_mode(Mode::Insert, false, window, cx); } else { self.switch_mode(Mode::Normal, false, window, cx); } } } // From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html // Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is // on a non-blank. This is because "cw" is interpreted as change-word, and a // word does not include the following white space. {Vi: "cw" when on a blank // followed by other blanks changes only the first blank; this is probably a // bug, because "dw" deletes all the blanks} fn expand_changed_word_selection( map: &DisplaySnapshot, selection: &mut Selection, times: Option, ignore_punctuation: bool, text_layout_details: &TextLayoutDetails, use_subword: bool, ) -> bool { let is_in_word = || { let classifier = map .buffer_snapshot .char_classifier_at(selection.start.to_point(map)); let in_word = map .buffer_chars_at(selection.head().to_offset(map, Bias::Left)) .next() .map(|(c, _)| !classifier.is_whitespace(c)) .unwrap_or_default(); in_word }; if (times.is_none() || times.unwrap() == 1) && is_in_word() { let next_char = map .buffer_chars_at( motion::next_char(map, selection.end, false).to_offset(map, Bias::Left), ) .next(); match next_char { Some((' ', _)) => selection.end = motion::next_char(map, selection.end, false), _ => { if use_subword { selection.end = motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false); } else { selection.end = motion::next_word_end(map, selection.end, ignore_punctuation, 1, false); } selection.end = motion::next_char(map, selection.end, false); } } true } else { let motion = if use_subword { Motion::NextSubwordStart { ignore_punctuation } } else { Motion::NextWordStart { ignore_punctuation } }; motion.expand_selection(map, selection, times, false, text_layout_details) } } #[cfg(test)] mod test { use indoc::indoc; use crate::test::NeovimBackedTestContext; #[gpui::test] async fn test_change_h(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.simulate("c h", "Teˇst").await.assert_matches(); cx.simulate("c h", "Tˇest").await.assert_matches(); cx.simulate("c h", "ˇTest").await.assert_matches(); cx.simulate( "c h", indoc! {" Test ˇtest"}, ) .await .assert_matches(); } #[gpui::test] async fn test_change_backspace(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.simulate("c backspace", "Teˇst").await.assert_matches(); cx.simulate("c backspace", "Tˇest").await.assert_matches(); cx.simulate("c backspace", "ˇTest").await.assert_matches(); cx.simulate( "c backspace", indoc! {" Test ˇtest"}, ) .await .assert_matches(); } #[gpui::test] async fn test_change_l(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.simulate("c l", "Teˇst").await.assert_matches(); cx.simulate("c l", "Tesˇt").await.assert_matches(); } #[gpui::test] async fn test_change_w(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.simulate("c w", "Teˇst").await.assert_matches(); cx.simulate("c w", "Tˇest test").await.assert_matches(); cx.simulate("c w", "Testˇ test").await.assert_matches(); cx.simulate("c w", "Tesˇt test").await.assert_matches(); cx.simulate( "c w", indoc! {" Test teˇst test"}, ) .await .assert_matches(); cx.simulate( "c w", indoc! {" Test tesˇt test"}, ) .await .assert_matches(); cx.simulate( "c w", indoc! {" Test test ˇ test"}, ) .await .assert_matches(); cx.simulate("c shift-w", "Test teˇst-test test") .await .assert_matches(); } #[gpui::test] async fn test_change_e(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.simulate("c e", "Teˇst Test").await.assert_matches(); cx.simulate("c e", "Tˇest test").await.assert_matches(); cx.simulate( "c e", indoc! {" Test teˇst test"}, ) .await .assert_matches(); cx.simulate( "c e", indoc! {" Test tesˇt test"}, ) .await .assert_matches(); cx.simulate( "c e", indoc! {" Test test ˇ test"}, ) .await .assert_matches(); cx.simulate("c shift-e", "Test teˇst-test test") .await .assert_matches(); } #[gpui::test] async fn test_change_b(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.simulate("c b", "Teˇst Test").await.assert_matches(); cx.simulate("c b", "Test ˇtest").await.assert_matches(); cx.simulate("c b", "Test1 test2 ˇtest3") .await .assert_matches(); cx.simulate( "c b", indoc! {" Test test ˇtest"}, ) .await .assert_matches(); cx.simulate( "c b", indoc! {" Test test ˇ test"}, ) .await .assert_matches(); cx.simulate("c shift-b", "Test test-test ˇtest") .await .assert_matches(); } #[gpui::test] async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.simulate( "c $", indoc! {" The qˇuick brown fox"}, ) .await .assert_matches(); cx.simulate( "c $", indoc! {" The quick ˇ brown fox"}, ) .await .assert_matches(); } #[gpui::test] async fn test_change_0(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.simulate( "c 0", indoc! {" The qˇuick brown fox"}, ) .await .assert_matches(); cx.simulate( "c 0", indoc! {" The quick ˇ brown fox"}, ) .await .assert_matches(); } #[gpui::test] async fn test_change_k(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.simulate( "c k", indoc! {" The quick brown ˇfox jumps over"}, ) .await .assert_matches(); cx.simulate( "c k", indoc! {" The quick brown fox jumps ˇover"}, ) .await .assert_matches(); cx.simulate( "c k", indoc! {" The qˇuick brown fox jumps over"}, ) .await .assert_matches(); cx.simulate( "c k", indoc! {" ˇ brown fox jumps over"}, ) .await .assert_matches(); } #[gpui::test] async fn test_change_j(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.simulate( "c j", indoc! {" The quick brown ˇfox jumps over"}, ) .await .assert_matches(); cx.simulate( "c j", indoc! {" The quick brown fox jumps ˇover"}, ) .await .assert_matches(); cx.simulate( "c j", indoc! {" The qˇuick brown fox jumps over"}, ) .await .assert_matches(); cx.simulate( "c j", indoc! {" The quick brown fox ˇ"}, ) .await .assert_matches(); } #[gpui::test] async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.simulate( "c shift-g", indoc! {" The quick brownˇ fox jumps over the lazy"}, ) .await .assert_matches(); cx.simulate( "c shift-g", indoc! {" The quick brownˇ fox jumps over the lazy"}, ) .await .assert_matches(); cx.simulate( "c shift-g", indoc! {" The quick brown fox jumps over the lˇazy"}, ) .await .assert_matches(); cx.simulate( "c shift-g", indoc! {" The quick brown fox jumps over ˇ"}, ) .await .assert_matches(); } #[gpui::test] async fn test_change_cc(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.simulate( "c c", indoc! {" The quick brownˇ fox jumps over the lazy"}, ) .await .assert_matches(); cx.simulate( "c c", indoc! {" ˇThe quick brown fox jumps over the lazy"}, ) .await .assert_matches(); cx.simulate( "c c", indoc! {" The quick broˇwn fox jumps over the lazy"}, ) .await .assert_matches(); } #[gpui::test] async fn test_change_gg(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.simulate( "c g g", indoc! {" The quick brownˇ fox jumps over the lazy"}, ) .await .assert_matches(); cx.simulate( "c g g", indoc! {" The quick brown fox jumps over the lˇazy"}, ) .await .assert_matches(); cx.simulate( "c g g", indoc! {" The qˇuick brown fox jumps over the lazy"}, ) .await .assert_matches(); cx.simulate( "c g g", indoc! {" ˇ brown fox jumps over the lazy"}, ) .await .assert_matches(); } #[gpui::test] async fn test_repeated_cj(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; for count in 1..=5 { cx.simulate_at_each_offset( &format!("c {count} j"), indoc! {" ˇThe quˇickˇ browˇn ˇ ˇfox ˇjumpsˇ-ˇoˇver ˇthe lazy dog "}, ) .await .assert_matches(); } } #[gpui::test] async fn test_repeated_cl(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; for count in 1..=5 { cx.simulate_at_each_offset( &format!("c {count} l"), indoc! {" ˇThe quˇickˇ browˇn ˇ ˇfox ˇjumpsˇ-ˇoˇver ˇthe lazy dog "}, ) .await .assert_matches(); } } #[gpui::test] async fn test_repeated_cb(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; for count in 1..=5 { cx.simulate_at_each_offset( &format!("c {count} b"), indoc! {" ˇThe quˇickˇ browˇn ˇ ˇfox ˇjumpsˇ-ˇoˇver ˇthe lazy dog "}, ) .await .assert_matches() } } #[gpui::test] async fn test_repeated_ce(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; for count in 1..=5 { cx.simulate_at_each_offset( &format!("c {count} e"), indoc! {" ˇThe quˇickˇ browˇn ˇ ˇfox ˇjumpsˇ-ˇoˇver ˇthe lazy dog "}, ) .await .assert_matches(); } } }