diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index e8bf95061c..2da8c26342 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -140,7 +140,8 @@ "c": "vim::VisualChange", "d": "vim::VisualDelete", "x": "vim::VisualDelete", - "y": "vim::VisualYank" + "y": "vim::VisualYank", + "p": "vim::VisualPaste" } }, { diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 1d02b26e4b..026144db64 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -149,6 +149,28 @@ impl SelectionsCollection { selections } + pub fn all_adjusted_display( + &self, + cx: &mut MutableAppContext, + ) -> (DisplaySnapshot, Vec>) { + if self.line_mode { + let selections = self.all::(cx); + let map = self.display_map(cx); + let result = selections + .into_iter() + .map(|mut selection| { + let new_range = map.expand_to_line(selection.range()); + selection.start = new_range.start; + selection.end = new_range.end; + selection.map(|point| point.to_display_point(&map)) + }) + .collect(); + (map, result) + } else { + self.all_display(cx) + } + } + pub fn disjoint_in_range<'a, D>( &self, range: Range, @@ -175,7 +197,7 @@ impl SelectionsCollection { } pub fn all_display( - &mut self, + &self, cx: &mut MutableAppContext, ) -> (DisplaySnapshot, Vec>) { let display_map = self.display_map(cx); diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 4c6dfd2d60..c5fb044dd3 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -194,11 +194,11 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex }); } -// Supports non empty selections so it can be bound and called from visual mode fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); if let Some(item) = cx.as_mut().read_from_clipboard() { let mut clipboard_text = Cow::Borrowed(item.text()); if let Some(mut clipboard_selections) = @@ -244,7 +244,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { // If the clipboard text was copied linewise, and the current selection // is empty, then paste the text after this line and move the selection // to the start of the pasted text - let range = if selection.is_empty() && linewise { + let insert_at = if linewise { let (point, _) = display_map .next_line_boundary(selection.start.to_point(&display_map)); @@ -255,37 +255,26 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { // Drop selection at the start of the next line let selection_point = Point::new(point.row + 1, 0); new_selections.push(selection.map(|_| selection_point.clone())); - point..point + point } else { - let mut selection = selection.clone(); - if !selection.reversed { - let mut adjusted = selection.end; - // Head is at the end of the selection. Adjust the end position to - // to include the character under the cursor. - *adjusted.column_mut() = adjusted.column() + 1; - adjusted = display_map.clip_point(adjusted, Bias::Right); - // If the selection is empty, move both the start and end forward one - // character - if selection.is_empty() { - selection.start = adjusted; - selection.end = adjusted; - } else { - selection.end = adjusted; - } - } + let mut point = selection.end; + // Paste the text after the current selection + *point.column_mut() = point.column() + 1; + let point = display_map + .clip_point(point, Bias::Right) + .to_point(&display_map); - let range = selection.map(|p| p.to_point(&display_map)).range(); - new_selections.push(selection.map(|_| range.start.clone())); - range + new_selections.push(selection.map(|_| point)); + point }; if linewise && to_insert.ends_with('\n') { edits.push(( - range, + insert_at..insert_at, &to_insert[0..to_insert.len().saturating_sub(1)], )) } else { - edits.push((range, to_insert)); + edits.push((insert_at..insert_at, to_insert)); } } drop(snapshot); @@ -299,6 +288,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { editor.insert(&clipboard_text, cx); } } + editor.set_clip_at_line_ends(true, cx); }); }); }); @@ -1155,10 +1145,13 @@ mod test { the la|zy dog"}); cx.simulate_keystroke("p"); - cx.assert_editor_state(indoc! {" - The quick brown - the lazy dog - |fox jumps over"}); + cx.assert_state( + indoc! {" + The quick brown + the lazy dog + |fox jumps over"}, + Mode::Normal, + ); cx.set_state( indoc! {" @@ -1171,14 +1164,17 @@ mod test { cx.set_state( indoc! {" The quick brown - fox jump|s over + fox jumps ove|r the lazy dog"}, Mode::Normal, ); cx.simulate_keystroke("p"); - cx.assert_editor_state(indoc! {" - The quick brown - fox jumps|jumps over - the lazy dog"}); + cx.assert_state( + indoc! {" + The quick brown + fox jumps over|jumps + the lazy dog"}, + Mode::Normal, + ); } } diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index 08ec4bd5e9..e13f9fda51 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/crates/vim/src/vim_test_context.rs @@ -125,6 +125,11 @@ impl<'a> VimTestContext<'a> { self.cx.set_state(text); } + pub fn assert_state(&mut self, text: &str, mode: Mode) { + self.assert_editor_state(text); + assert_eq!(self.mode(), mode); + } + pub fn assert_binding( &mut self, keystrokes: [&str; COUNT], diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 3020db5e4c..7028f00e92 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,17 +1,20 @@ +use std::borrow::Cow; + use collections::HashMap; -use editor::{display_map::ToDisplayPoint, Autoscroll, Bias}; +use editor::{display_map::ToDisplayPoint, Autoscroll, Bias, ClipboardSelection}; use gpui::{actions, MutableAppContext, ViewContext}; use language::SelectionGoal; use workspace::Workspace; use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim}; -actions!(vim, [VisualDelete, VisualChange, VisualYank]); +actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(change); cx.add_action(delete); cx.add_action(yank); + cx.add_action(paste); } pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) { @@ -136,7 +139,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext) vim.update_active_editor(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let line_mode = editor.selections.line_mode; - if !editor.selections.line_mode { + if !line_mode { editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { if !selection.reversed { @@ -159,6 +162,114 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext) }); } +pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + vim.update_active_editor(cx, |editor, cx| { + editor.transact(cx, |editor, cx| { + if let Some(item) = cx.as_mut().read_from_clipboard() { + copy_selections_content(editor, editor.selections.line_mode, cx); + let mut clipboard_text = Cow::Borrowed(item.text()); + if let Some(mut clipboard_selections) = + item.metadata::>() + { + let (display_map, selections) = editor.selections.all_adjusted_display(cx); + let all_selections_were_entire_line = + clipboard_selections.iter().all(|s| s.is_entire_line); + if clipboard_selections.len() != selections.len() { + let mut newline_separated_text = String::new(); + let mut clipboard_selections = + clipboard_selections.drain(..).peekable(); + let mut ix = 0; + while let Some(clipboard_selection) = clipboard_selections.next() { + newline_separated_text + .push_str(&clipboard_text[ix..ix + clipboard_selection.len]); + ix += clipboard_selection.len; + if clipboard_selections.peek().is_some() { + newline_separated_text.push('\n'); + } + } + clipboard_text = Cow::Owned(newline_separated_text); + } + + let mut new_selections = Vec::new(); + editor.buffer().update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + let mut start_offset = 0; + let mut edits = Vec::new(); + for (ix, selection) in selections.iter().enumerate() { + let to_insert; + let linewise; + if let Some(clipboard_selection) = clipboard_selections.get(ix) { + let end_offset = start_offset + clipboard_selection.len; + to_insert = &clipboard_text[start_offset..end_offset]; + linewise = clipboard_selection.is_entire_line; + start_offset = end_offset; + } else { + to_insert = clipboard_text.as_str(); + linewise = all_selections_were_entire_line; + } + + let mut selection = selection.clone(); + if !selection.reversed { + let mut adjusted = selection.end; + // Head is at the end of the selection. Adjust the end position to + // to include the character under the cursor. + *adjusted.column_mut() = adjusted.column() + 1; + adjusted = display_map.clip_point(adjusted, Bias::Right); + // If the selection is empty, move both the start and end forward one + // character + if selection.is_empty() { + selection.start = adjusted; + selection.end = adjusted; + } else { + selection.end = adjusted; + } + } + + let range = selection.map(|p| p.to_point(&display_map)).range(); + + let new_position = if linewise { + edits.push((range.start..range.start, "\n")); + let mut new_position = range.start.clone(); + new_position.column = 0; + new_position.row += 1; + new_position + } else { + range.start.clone() + }; + + new_selections.push(selection.map(|_| new_position.clone())); + + if linewise && to_insert.ends_with('\n') { + edits.push(( + range.clone(), + &to_insert[0..to_insert.len().saturating_sub(1)], + )) + } else { + edits.push((range.clone(), to_insert)); + } + + if linewise { + edits.push((range.end..range.end, "\n")); + } + } + drop(snapshot); + buffer.edit_with_autoindent(edits, cx); + }); + + editor.change_selections(Some(Autoscroll::Fit), cx, |s| { + s.select(new_selections) + }); + } else { + editor.insert(&clipboard_text, cx); + } + } + }); + }); + vim.switch_mode(Mode::Normal, cx); + }); +} + #[cfg(test)] mod test { use indoc::indoc; @@ -607,4 +718,62 @@ mod test { quick brown fox jumps o"})); } + + #[gpui::test] + async fn test_visual_paste(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state( + indoc! {" + The quick brown + fox [jump}s over + the lazy dog"}, + Mode::Visual { line: false }, + ); + cx.simulate_keystroke("y"); + cx.set_state( + indoc! {" + The quick brown + fox jump|s over + the lazy dog"}, + Mode::Normal, + ); + cx.simulate_keystroke("p"); + cx.assert_state( + indoc! {" + The quick brown + fox jumps|jumps over + the lazy dog"}, + Mode::Normal, + ); + + cx.set_state( + indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}, + Mode::Visual { line: true }, + ); + cx.simulate_keystroke("d"); + cx.assert_state( + indoc! {" + The quick brown + the la|zy dog"}, + Mode::Normal, + ); + cx.set_state( + indoc! {" + The quick brown + the [laz}y dog"}, + Mode::Visual { line: false }, + ); + cx.simulate_keystroke("p"); + cx.assert_state( + indoc! {" + The quick brown + the + |fox jumps over + dog"}, + Mode::Normal, + ); + } }