From 082036161fd3815c831ceedfd28ba15b0ed6eb9f Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 19 May 2022 17:42:30 -0700 Subject: [PATCH] Enable copy and paste in vim mode --- assets/keymaps/vim.json | 3 +- crates/editor/src/editor.rs | 2 +- crates/editor/src/element.rs | 2 +- crates/text/src/selection.rs | 5 ++ crates/vim/src/normal.rs | 124 +++++++++++++++++++++++++++++++- crates/vim/src/normal/change.rs | 22 ++---- crates/vim/src/normal/delete.rs | 3 +- crates/vim/src/utils.rs | 26 +++++++ crates/vim/src/vim.rs | 14 ++-- crates/vim/src/visual.rs | 16 +++-- 10 files changed, 183 insertions(+), 34 deletions(-) create mode 100644 crates/vim/src/utils.rs diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index e5fdf44d3e..00e7fdba2c 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -76,7 +76,8 @@ "shift-V": [ "vim::SwitchMode", "VisualLine" - ] + ], + "p": "vim::Paste" } }, { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 219fcba22b..d80b03da9e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3,7 +3,7 @@ mod element; pub mod items; pub mod movement; mod multi_buffer; -mod selections_collection; +pub mod selections_collection; #[cfg(test)] mod test; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a794ac7edd..3ef169a2e0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -489,7 +489,7 @@ impl EditorElement { cx: &mut PaintContext, ) { if range.start != range.end || line_mode { - let row_range = if range.end.column() == 0 { + let row_range = if range.end.column() == 0 && !line_mode { cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row) } else { cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row) diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 8dcc3fc7f1..fd8d57ca9f 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -1,6 +1,7 @@ use crate::Anchor; use crate::{rope::TextDimension, BufferSnapshot}; use std::cmp::Ordering; +use std::ops::Range; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum SelectionGoal { @@ -83,6 +84,10 @@ impl Selection { self.goal = new_goal; self.reversed = false; } + + pub fn range(&self) -> Range { + self.start..self.end + } } impl Selection { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 2a391676fa..d9b5d470e7 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1,6 +1,8 @@ mod change; mod delete; +use std::borrow::Cow; + use crate::{ motion::Motion, state::{Mode, Operator}, @@ -8,9 +10,9 @@ use crate::{ }; use change::init as change_init; use collections::HashSet; -use editor::{Autoscroll, Bias, DisplayPoint}; +use editor::{Autoscroll, Bias, ClipboardSelection, DisplayPoint}; use gpui::{actions, MutableAppContext, ViewContext}; -use language::SelectionGoal; +use language::{Point, SelectionGoal}; use workspace::Workspace; use self::{change::change_over, delete::delete_over}; @@ -27,6 +29,8 @@ actions!( DeleteRight, ChangeToEndOfLine, DeleteToEndOfLine, + Paste, + Yank, ] ); @@ -56,6 +60,7 @@ pub fn init(cx: &mut MutableAppContext) { delete_over(vim, Motion::EndOfLine, cx); }) }); + cx.add_action(paste); change_init(cx); } @@ -187,6 +192,98 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex }); } +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| { + if let Some(item) = cx.as_mut().read_from_clipboard() { + let mut clipboard_text = Cow::Borrowed(item.text()); + if let Some(mut clipboard_selections) = + item.metadata::>() + { + 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() { + 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; + } + + // 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 (point, _) = display_map + .next_line_boundary(selection.start.to_point(&display_map)); + + if !to_insert.starts_with('\n') { + // Add newline before pasted text so that it shows up + edits.push((point..point, "\n")); + } + // 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 + } else { + let range = selection.map(|p| p.to_point(&display_map)).range(); + new_selections.push(selection.map(|_| range.start.clone())); + range + }; + + if linewise && to_insert.ends_with('\n') { + edits.push(( + range, + &to_insert[0..to_insert.len().saturating_sub(1)], + )) + } else { + edits.push((range, to_insert)); + } + } + 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); + } + } + }); + }); + }); +} + #[cfg(test)] mod test { use indoc::indoc; @@ -1026,4 +1123,27 @@ mod test { brown fox"}, ); } + + #[gpui::test] + async fn test_p(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state( + indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}, + Mode::Normal, + ); + + cx.simulate_keystrokes(["d", "d"]); + cx.assert_editor_state(indoc! {" + The quick brown + the la|zy dog"}); + + cx.simulate_keystroke("p"); + cx.assert_editor_state(indoc! {" + The quick brown + the lazy dog + |fox jumps over"}); + } } diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 0636b4b1ef..7f417fd31e 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -1,6 +1,6 @@ -use crate::{motion::Motion, state::Mode, Vim}; -use editor::{char_kind, movement, Autoscroll, ClipboardSelection}; -use gpui::{impl_actions, ClipboardItem, MutableAppContext, ViewContext}; +use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim}; +use editor::{char_kind, movement, Autoscroll}; +use gpui::{impl_actions, MutableAppContext, ViewContext}; use serde::Deserialize; use workspace::Workspace; @@ -22,26 +22,13 @@ pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) { editor.transact(cx, |editor, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now editor.set_clip_at_line_ends(false, cx); - let mut text = String::new(); - let buffer = editor.buffer().read(cx).snapshot(cx); - let mut clipboard_selections = Vec::with_capacity(editor.selections.count()); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { s.move_with(|map, selection| { motion.expand_selection(map, selection, false); - let mut len = 0; - let range = selection.start.to_point(map)..selection.end.to_point(map); - for chunk in buffer.text_for_range(range) { - text.push_str(chunk); - len += chunk.len(); - } - clipboard_selections.push(ClipboardSelection { - len, - is_entire_line: motion.linewise(), - }); }); }); + copy_selections_content(editor, motion.linewise(), cx); editor.insert(&"", cx); - cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); }); }); vim.switch_mode(Mode::Insert, cx) @@ -79,6 +66,7 @@ fn change_word( }); }); }); + copy_selections_content(editor, false, cx); editor.insert(&"", cx); }); }); diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index b44f0a1f34..cea607e9f3 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -1,4 +1,4 @@ -use crate::{motion::Motion, Vim}; +use crate::{motion::Motion, utils::copy_selections_content, Vim}; use collections::HashMap; use editor::{Autoscroll, Bias}; use gpui::MutableAppContext; @@ -15,6 +15,7 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) { original_columns.insert(selection.id, original_head.column()); }); }); + copy_selections_content(editor, motion.linewise(), cx); editor.insert(&"", cx); // Fixup cursor position after the deletion diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs new file mode 100644 index 0000000000..1cd5f13608 --- /dev/null +++ b/crates/vim/src/utils.rs @@ -0,0 +1,26 @@ +use editor::{ClipboardSelection, Editor}; +use gpui::{ClipboardItem, MutableAppContext}; +use language::Point; + +pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut MutableAppContext) { + let selections = editor.selections.all::(cx); + let buffer = editor.buffer().read(cx).snapshot(cx); + let mut text = String::new(); + let mut clipboard_selections = Vec::with_capacity(selections.len()); + { + for selection in selections.iter() { + let initial_len = text.len(); + let start = selection.start; + let end = selection.end; + for chunk in buffer.text_for_range(start..end) { + text.push_str(chunk); + } + clipboard_selections.push(ClipboardSelection { + len: text.len() - initial_len, + is_entire_line: linewise, + }); + } + } + + cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 115536e6a5..00ef989874 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -6,6 +6,7 @@ mod insert; mod motion; mod normal; mod state; +mod utils; mod visual; use collections::HashMap; @@ -140,11 +141,14 @@ impl Vim { } if state.empty_selections_only() { - editor.change_selections(None, cx, |s| { - s.move_with(|_, selection| { - selection.collapse_to(selection.head(), selection.goal) - }); - }) + // Defer so that access to global settings object doesn't panic + cx.defer(|editor, cx| { + editor.change_selections(None, cx, |s| { + s.move_with(|_, selection| { + selection.collapse_to(selection.head(), selection.goal) + }); + }) + }); } }); } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 0a7517bfb8..480c7e07b6 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -3,7 +3,7 @@ use editor::{Autoscroll, Bias}; use gpui::{actions, MutableAppContext, ViewContext}; use workspace::Workspace; -use crate::{motion::Motion, state::Mode, Vim}; +use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim}; actions!( vim, @@ -41,7 +41,7 @@ pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) { // Head was at the end of the selection, and now is at the start. We need to move the end // forward by one if possible in order to compensate for this change. *selection.end.column_mut() = selection.end.column() + 1; - selection.end = map.clip_point(selection.end, Bias::Left); + selection.end = map.clip_point(selection.end, Bias::Right); } }); }); @@ -63,6 +63,7 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { - vim.switch_mode(Mode::Normal, cx); vim.update_active_editor(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { s.move_with(|map, selection| { if !selection.reversed { - // Head was at the end of the selection, and now is at the start. We need to move the end - // forward by one if possible in order to compensate for this change. + // Head is at the end of the selection. Adjust the end position to + // to include the character under the cursor. *selection.end.column_mut() = selection.end.column() + 1; - selection.end = map.clip_point(selection.end, Bias::Left); + selection.end = map.clip_point(selection.end, Bias::Right); } }); }); + copy_selections_content(editor, false, cx); editor.insert("", cx); // Fixup cursor position after the deletion @@ -112,6 +114,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext