From ee007f901a025e3377c90f51b4227e20c09b0e16 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Fri, 8 Jul 2022 10:57:02 -0700 Subject: [PATCH] fix pasting at the end of the line in normal mode --- assets/keymaps/vim.json | 2 +- crates/vim/src/normal.rs | 66 +++++------ crates/vim/src/vim_test_context.rs | 5 + crates/vim/src/visual.rs | 173 ++++++++++++++++++++++++++++- 4 files changed, 207 insertions(+), 39 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 36b4261fac..2da8c26342 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -141,7 +141,7 @@ "d": "vim::VisualDelete", "x": "vim::VisualDelete", "y": "vim::VisualYank", - "p": "vim::Paste" + "p": "vim::VisualPaste" } }, { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 9dd2274792..c5fb044dd3 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -7,7 +7,6 @@ use std::borrow::Cow; use crate::{ motion::Motion, state::{Mode, Operator}, - utils::copy_selections_content, Vim, }; use change::init as change_init; @@ -195,18 +194,17 @@ 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() { - 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 (display_map, selections) = editor.selections.all_display(cx); let all_selections_were_entire_line = clipboard_selections.iter().all(|s| s.is_entire_line); if clipboard_selections.len() != selections.len() { @@ -246,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)); @@ -257,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); @@ -301,6 +288,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { editor.insert(&clipboard_text, cx); } } + editor.set_clip_at_line_ends(true, cx); }); }); }); @@ -1157,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! {" @@ -1173,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 57d0174703..f0ef9b9cd8 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 203477198f..7028f00e92 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,5 +1,7 @@ +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; @@ -12,6 +14,7 @@ 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, + ); + } }