Detailed changes
@@ -287,6 +287,12 @@
"shift-o": "vim::InsertLineAbove",
"~": "vim::ChangeCase",
"p": "vim::Paste",
+ "shift-p": [
+ "vim::Paste",
+ {
+ "before": true
+ }
+ ],
"u": "editor::Undo",
"ctrl-r": "editor::Redo",
"/": "vim::Search",
@@ -375,7 +381,13 @@
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"y": "vim::VisualYank",
- "p": "vim::VisualPaste",
+ "p": "vim::Paste",
+ "shift-p": [
+ "vim::Paste",
+ {
+ "preserveClipboard": true
+ }
+ ],
"s": "vim::Substitute",
"c": "vim::Substitute",
"~": "vim::ChangeCase",
@@ -1736,6 +1736,31 @@ impl Editor {
});
}
+ pub fn edit_with_block_indent<I, S, T>(
+ &mut self,
+ edits: I,
+ original_indent_columns: Vec<u32>,
+ cx: &mut ViewContext<Self>,
+ ) where
+ I: IntoIterator<Item = (Range<S>, T)>,
+ S: ToOffset,
+ T: Into<Arc<str>>,
+ {
+ if self.read_only {
+ return;
+ }
+
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ edits,
+ Some(AutoindentMode::Block {
+ original_indent_columns,
+ }),
+ cx,
+ )
+ });
+ }
+
fn select(&mut self, phase: SelectPhase, cx: &mut ViewContext<Self>) {
self.hide_context_menu(cx);
@@ -4741,6 +4766,7 @@ impl Editor {
let mut clipboard_selections = Vec::with_capacity(selections.len());
{
let max_point = buffer.max_point();
+ let mut is_first = true;
for selection in &mut selections {
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
@@ -4748,6 +4774,11 @@ impl Editor {
selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0));
selection.goal = SelectionGoal::None;
}
+ if is_first {
+ is_first = false;
+ } else {
+ text += "\n";
+ }
let mut len = 0;
for chunk in buffer.text_for_range(selection.start..selection.end) {
text.push_str(chunk);
@@ -4778,6 +4809,7 @@ impl Editor {
let mut clipboard_selections = Vec::with_capacity(selections.len());
{
let max_point = buffer.max_point();
+ let mut is_first = true;
for selection in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
@@ -4786,6 +4818,11 @@ impl Editor {
start = Point::new(start.row, 0);
end = cmp::min(max_point, Point::new(end.row + 1, 0));
}
+ if is_first {
+ is_first = false;
+ } else {
+ text += "\n";
+ }
let mut len = 0;
for chunk in buffer.text_for_range(start..end) {
text.push_str(chunk);
@@ -4805,7 +4842,7 @@ impl Editor {
pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
if let Some(item) = cx.read_from_clipboard() {
- let mut clipboard_text = Cow::Borrowed(item.text());
+ let clipboard_text = Cow::Borrowed(item.text());
if let Some(mut clipboard_selections) = item.metadata::<Vec<ClipboardSelection>>() {
let old_selections = this.selections.all::<usize>(cx);
let all_selections_were_entire_line =
@@ -4813,18 +4850,7 @@ impl Editor {
let first_selection_indent_column =
clipboard_selections.first().map(|s| s.first_line_indent);
if clipboard_selections.len() != old_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);
+ clipboard_selections.drain(..);
}
this.buffer.update(cx, |buffer, cx| {
@@ -4840,8 +4866,9 @@ impl Editor {
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];
+ dbg!(start_offset, end_offset, &clipboard_text, &to_insert);
entire_line = clipboard_selection.is_entire_line;
- start_offset = end_offset;
+ start_offset = end_offset + 1;
original_indent_column =
Some(clipboard_selection.first_line_indent);
} else {
@@ -162,6 +162,15 @@ impl<'a> EditorLspTestContext<'a> {
LanguageConfig {
name: "Typescript".into(),
path_suffixes: vec!["ts".to_string()],
+ brackets: language::BracketPairConfig {
+ pairs: vec![language::BracketPair {
+ start: "{".to_string(),
+ end: "}".to_string(),
+ close: true,
+ newline: true,
+ }],
+ disabled_scopes_by_bracket_ix: Default::default(),
+ },
word_characters,
..Default::default()
},
@@ -174,6 +183,23 @@ impl<'a> EditorLspTestContext<'a> {
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)"#})),
+ indents: Some(Cow::from(indoc! {r#"
+ [
+ (call_expression)
+ (assignment_expression)
+ (member_expression)
+ (lexical_declaration)
+ (variable_declaration)
+ (assignment_expression)
+ (if_statement)
+ (for_statement)
+ ] @indent
+
+ (_ "[" "]" @end) @indent
+ (_ "<" ">" @end) @indent
+ (_ "{" "}" @end) @indent
+ (_ "(" ")" @end) @indent
+ "#})),
..Default::default()
})
.expect("Could not parse queries");
@@ -1,12 +1,13 @@
mod case;
mod change;
mod delete;
+mod paste;
mod scroll;
mod search;
pub mod substitute;
mod yank;
-use std::{borrow::Cow, sync::Arc};
+use std::sync::Arc;
use crate::{
motion::Motion,
@@ -14,13 +15,11 @@ use crate::{
state::{Mode, Operator},
Vim,
};
-use collections::{HashMap, HashSet};
-use editor::{
- display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, Bias, ClipboardSelection,
- DisplayPoint,
-};
+use collections::HashSet;
+use editor::scroll::autoscroll::Autoscroll;
+use editor::{Bias, DisplayPoint};
use gpui::{actions, AppContext, ViewContext, WindowContext};
-use language::{AutoindentMode, Point, SelectionGoal};
+use language::SelectionGoal;
use log::error;
use workspace::Workspace;
@@ -44,7 +43,6 @@ actions!(
DeleteRight,
ChangeToEndOfLine,
DeleteToEndOfLine,
- Paste,
Yank,
Substitute,
ChangeCase,
@@ -89,9 +87,8 @@ pub fn init(cx: &mut AppContext) {
delete_motion(vim, Motion::EndOfLine, times, cx);
})
});
- cx.add_action(paste);
-
scroll::init(cx);
+ paste::init(cx);
}
pub fn normal_motion(
@@ -250,144 +247,6 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
});
}
-fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
- 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.read_from_clipboard() {
- let mut clipboard_text = Cow::Borrowed(item.text());
- if let Some(mut clipboard_selections) =
- item.metadata::<Vec<ClipboardSelection>>()
- {
- 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);
- }
-
- // If the pasted text is a single line, the cursor should be placed after
- // the newly pasted text. This is easiest done with an anchor after the
- // insertion, and then with a fixup to move the selection back one position.
- // However if the pasted text is linewise, the cursor should be placed at the start
- // of the new text on the following line. This is easiest done with a manually adjusted
- // point.
- // This enum lets us represent both cases
- enum NewPosition {
- Inside(Point),
- After(Anchor),
- }
- let mut new_selections: HashMap<usize, NewPosition> = Default::default();
- 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 insert_at = if 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
- new_selections.insert(
- selection.id,
- NewPosition::Inside(Point::new(point.row + 1, 0)),
- );
- point
- } else {
- 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);
-
- new_selections.insert(
- selection.id,
- if to_insert.contains('\n') {
- NewPosition::Inside(point)
- } else {
- NewPosition::After(snapshot.anchor_after(point))
- },
- );
- point
- };
-
- if linewise && to_insert.ends_with('\n') {
- edits.push((
- insert_at..insert_at,
- &to_insert[0..to_insert.len().saturating_sub(1)],
- ))
- } else {
- edits.push((insert_at..insert_at, to_insert));
- }
- }
- drop(snapshot);
- buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
- });
-
- editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
- s.move_with(|map, selection| {
- if let Some(new_position) = new_selections.get(&selection.id) {
- match new_position {
- NewPosition::Inside(new_point) => {
- selection.collapse_to(
- new_point.to_display_point(map),
- SelectionGoal::None,
- );
- }
- NewPosition::After(after_point) => {
- let mut new_point = after_point.to_display_point(map);
- *new_point.column_mut() =
- new_point.column().saturating_sub(1);
- new_point = map.clip_point(new_point, Bias::Left);
- selection.collapse_to(new_point, SelectionGoal::None);
- }
- }
- }
- });
- });
- } else {
- editor.insert(&clipboard_text, cx);
- }
- }
- editor.set_clip_at_line_ends(true, cx);
- });
- });
- });
-}
-
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
@@ -883,36 +742,6 @@ mod test {
.await;
}
- #[gpui::test]
- async fn test_p(cx: &mut gpui::TestAppContext) {
- let mut cx = NeovimBackedTestContext::new(cx).await;
- cx.set_shared_state(indoc! {"
- The quick brown
- fox juˇmps over
- the lazy dog"})
- .await;
-
- cx.simulate_shared_keystrokes(["d", "d"]).await;
- cx.assert_state_matches().await;
-
- cx.simulate_shared_keystroke("p").await;
- cx.assert_state_matches().await;
-
- cx.set_shared_state(indoc! {"
- The quick brown
- fox ˇjumps over
- the lazy dog"})
- .await;
- cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
- cx.set_shared_state(indoc! {"
- The quick brown
- fox jumps oveˇr
- the lazy dog"})
- .await;
- cx.simulate_shared_keystroke("p").await;
- cx.assert_state_matches().await;
- }
-
#[gpui::test]
async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
@@ -0,0 +1,468 @@
+use std::{borrow::Cow, cmp};
+
+use editor::{
+ display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, ClipboardSelection,
+ DisplayPoint,
+};
+use gpui::{impl_actions, AppContext, ViewContext};
+use language::{Bias, SelectionGoal};
+use serde::Deserialize;
+use workspace::Workspace;
+
+use crate::{state::Mode, utils::copy_selections_content, Vim};
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct Paste {
+ #[serde(default)]
+ before: bool,
+ #[serde(default)]
+ preserve_clipboard: bool,
+}
+
+impl_actions!(vim, [Paste]);
+
+pub(crate) fn init(cx: &mut AppContext) {
+ cx.add_action(paste);
+}
+
+fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
+ 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);
+
+ let Some(item) = cx.read_from_clipboard() else {
+ return
+ };
+ let clipboard_text = Cow::Borrowed(item.text());
+ if clipboard_text.is_empty() {
+ return;
+ }
+
+ if !action.preserve_clipboard && vim.state().mode.is_visual() {
+ copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx);
+ }
+
+ // if we are copying from multi-cursor (of visual block mode), we want
+ // to
+ let clipboard_selections =
+ item.metadata::<Vec<ClipboardSelection>>()
+ .filter(|clipboard_selections| {
+ clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine
+ });
+
+ let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
+
+ // unlike zed, if you have a multi-cursor selection from vim block mode,
+ // pasting it will paste it on subsequent lines, even if you don't yet
+ // have a cursor there.
+ let mut selections_to_process = Vec::new();
+ let mut i = 0;
+ while i < current_selections.len() {
+ selections_to_process
+ .push((current_selections[i].start..current_selections[i].end, true));
+ i += 1;
+ }
+ if let Some(clipboard_selections) = clipboard_selections.as_ref() {
+ let left = current_selections
+ .iter()
+ .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
+ .min()
+ .unwrap();
+ let mut row = current_selections.last().unwrap().end.row() + 1;
+ while i < clipboard_selections.len() {
+ let cursor =
+ display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
+ selections_to_process.push((cursor..cursor, false));
+ i += 1;
+ row += 1;
+ }
+ }
+
+ let first_selection_indent_column =
+ clipboard_selections.as_ref().and_then(|zed_selections| {
+ zed_selections
+ .first()
+ .map(|selection| selection.first_line_indent)
+ });
+ let before = action.before || vim.state().mode == Mode::VisualLine;
+
+ let mut edits = Vec::new();
+ let mut new_selections = Vec::new();
+ let mut original_indent_columns = Vec::new();
+ let mut start_offset = 0;
+
+ for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
+ let (mut to_insert, original_indent_column) =
+ if let Some(clipboard_selections) = &clipboard_selections {
+ if let Some(clipboard_selection) = clipboard_selections.get(ix) {
+ let end_offset = start_offset + clipboard_selection.len;
+ let text = clipboard_text[start_offset..end_offset].to_string();
+ start_offset = end_offset + 1;
+ (text, Some(clipboard_selection.first_line_indent))
+ } else {
+ ("".to_string(), first_selection_indent_column)
+ }
+ } else {
+ (clipboard_text.to_string(), first_selection_indent_column)
+ };
+ let line_mode = to_insert.ends_with("\n");
+ let is_multiline = to_insert.contains("\n");
+
+ if line_mode && !before {
+ if selection.is_empty() {
+ to_insert =
+ "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
+ } else {
+ to_insert = "\n".to_owned() + &to_insert;
+ }
+ } else if !line_mode && vim.state().mode == Mode::VisualLine {
+ to_insert = to_insert + "\n";
+ }
+
+ let display_range = if !selection.is_empty() {
+ selection.start..selection.end
+ } else if line_mode {
+ let point = if before {
+ movement::line_beginning(&display_map, selection.start, false)
+ } else {
+ movement::line_end(&display_map, selection.start, false)
+ };
+ point..point
+ } else {
+ let point = if before {
+ selection.start
+ } else {
+ movement::saturating_right(&display_map, selection.start)
+ };
+ point..point
+ };
+
+ let point_range = display_range.start.to_point(&display_map)
+ ..display_range.end.to_point(&display_map);
+ let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
+ display_map.buffer_snapshot.anchor_before(point_range.start)
+ } else {
+ display_map.buffer_snapshot.anchor_after(point_range.end)
+ };
+
+ if *preserve {
+ new_selections.push((anchor, line_mode, is_multiline));
+ }
+ edits.push((point_range, to_insert));
+ original_indent_columns.extend(original_indent_column);
+ }
+
+ editor.edit_with_block_indent(edits, original_indent_columns, cx);
+
+ // in line_mode vim will insert the new text on the next (or previous if before) line
+ // and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
+ // otherwise vim will insert the next text at (or before) the current cursor position,
+ // the cursor will go to the last (or first, if is_multiline) inserted character.
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.replace_cursors_with(|map| {
+ let mut cursors = Vec::new();
+ for (anchor, line_mode, is_multiline) in &new_selections {
+ let mut cursor = anchor.to_display_point(map);
+ if *line_mode {
+ if !before {
+ cursor =
+ movement::down(map, cursor, SelectionGoal::None, false).0;
+ }
+ cursor = movement::indented_line_beginning(map, cursor, true);
+ } else if !is_multiline {
+ cursor = movement::saturating_left(map, cursor)
+ }
+ cursors.push(cursor);
+ if vim.state().mode == Mode::VisualBlock {
+ break;
+ }
+ }
+
+ cursors
+ });
+ })
+ });
+ });
+ vim.switch_mode(Mode::Normal, true, cx);
+ });
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{
+ state::Mode,
+ test::{NeovimBackedTestContext, VimTestContext},
+ };
+ use indoc::indoc;
+
+ #[gpui::test]
+ async fn test_paste(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ // single line
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox ˇjumps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
+ cx.assert_shared_clipboard("jumps o").await;
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox jumps oveˇr
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystroke("p").await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox jumps overjumps ˇo
+ the lazy dog"})
+ .await;
+
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox jumps oveˇr
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystroke("shift-p").await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox jumps ovejumps ˇor
+ the lazy dog"})
+ .await;
+
+ // line mode
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox juˇmps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["d", "d"]).await;
+ cx.assert_shared_clipboard("fox jumps over\n").await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ the laˇzy dog"})
+ .await;
+ cx.simulate_shared_keystroke("p").await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ the lazy dog
+ ˇfox jumps over"})
+ .await;
+ cx.simulate_shared_keystrokes(["k", "shift-p"]).await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ ˇfox jumps over
+ the lazy dog
+ fox jumps over"})
+ .await;
+
+ // multiline, cursor to first character of pasted text.
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox jumps ˇover
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["v", "j", "y"]).await;
+ cx.assert_shared_clipboard("over\nthe lazy do").await;
+
+ cx.simulate_shared_keystroke("p").await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox jumps oˇover
+ the lazy dover
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["u", "shift-p"]).await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox jumps ˇover
+ the lazy doover
+ the lazy dog"})
+ .await;
+ }
+
+ #[gpui::test]
+ async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ // copy in visual mode
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox jˇumps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["v", "i", "w", "y"]).await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox ˇjumps over
+ the lazy dog"})
+ .await;
+ // paste in visual mode
+ cx.simulate_shared_keystrokes(["w", "v", "i", "w", "p"])
+ .await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox jumps jumpˇs
+ the lazy dog"})
+ .await;
+ cx.assert_shared_clipboard("over").await;
+ // paste in visual line mode
+ cx.simulate_shared_keystrokes(["up", "shift-v", "shift-p"])
+ .await;
+ cx.assert_shared_state(indoc! {"
+ ˇover
+ fox jumps jumps
+ the lazy dog"})
+ .await;
+ cx.assert_shared_clipboard("over").await;
+ // paste in visual block mode
+ cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "p"])
+ .await;
+ cx.assert_shared_state(indoc! {"
+ oveˇrver
+ overox jumps jumps
+ overhe lazy dog"})
+ .await;
+
+ // copy in visual line mode
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox juˇmps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ the laˇzy dog"})
+ .await;
+ // paste in visual mode
+ cx.simulate_shared_keystrokes(["v", "i", "w", "p"]).await;
+ cx.assert_shared_state(
+ &indoc! {"
+ The quick brown
+ the_
+ ˇfox jumps over
+ _dog"}
+ .replace("_", " "), // Hack for trailing whitespace
+ )
+ .await;
+ cx.assert_shared_clipboard("lazy").await;
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox juˇmps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ the laˇzy dog"})
+ .await;
+ // paste in visual line mode
+ cx.simulate_shared_keystrokes(["k", "shift-v", "p"]).await;
+ cx.assert_shared_state(indoc! {"
+ ˇfox jumps over
+ the lazy dog"})
+ .await;
+ cx.assert_shared_clipboard("The quick brown\n").await;
+ }
+
+ #[gpui::test]
+ async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ // copy in visual block mode
+ cx.set_shared_state(indoc! {"
+ The ˇquick brown
+ fox jumps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["ctrl-v", "2", "j", "y"])
+ .await;
+ cx.assert_shared_clipboard("q\nj\nl").await;
+ cx.simulate_shared_keystrokes(["p"]).await;
+ cx.assert_shared_state(indoc! {"
+ The qˇquick brown
+ fox jjumps over
+ the llazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
+ .await;
+ cx.assert_shared_state(indoc! {"
+ The ˇq brown
+ fox jjjumps over
+ the lllazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
+ .await;
+
+ cx.set_shared_state(indoc! {"
+ The ˇquick brown
+ fox jumps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["ctrl-v", "j", "y"]).await;
+ cx.assert_shared_clipboard("q\nj").await;
+ cx.simulate_shared_keystrokes(["l", "ctrl-v", "2", "j", "shift-p"])
+ .await;
+ cx.assert_shared_state(indoc! {"
+ The qˇqick brown
+ fox jjmps over
+ the lzy dog"})
+ .await;
+
+ cx.simulate_shared_keystrokes(["shift-v", "p"]).await;
+ cx.assert_shared_state(indoc! {"
+ ˇq
+ j
+ fox jjmps over
+ the lzy dog"})
+ .await;
+ }
+
+ #[gpui::test]
+ async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new_typescript(cx).await;
+
+ cx.set_state(
+ indoc! {"
+ class A {ˇ
+ }
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes(["o", "a", "(", ")", "{", "escape"]);
+ cx.assert_state(
+ indoc! {"
+ class A {
+ a()ˇ{}
+ }
+ "},
+ Mode::Normal,
+ );
+ // cursor goes to the first non-blank character in the line;
+ cx.simulate_keystrokes(["y", "y", "p"]);
+ cx.assert_state(
+ indoc! {"
+ class A {
+ a(){}
+ ˇa(){}
+ }
+ "},
+ Mode::Normal,
+ );
+ // indentation is preserved when pasting
+ cx.simulate_keystrokes(["u", "shift-v", "up", "y", "shift-p"]);
+ cx.assert_state(
+ indoc! {"
+ ˇclass A {
+ a(){}
+ class A {
+ a(){}
+ }
+ "},
+ Mode::Normal,
+ );
+ }
+}
@@ -129,14 +129,23 @@ impl<'a> NeovimBackedTestContext<'a> {
pub async fn assert_shared_state(&mut self, marked_text: &str) {
let neovim = self.neovim_state().await;
- if neovim != marked_text {
- let initial_state = self
- .last_set_state
- .as_ref()
- .unwrap_or(&"N/A".to_string())
- .clone();
- panic!(
- indoc! {"Test is incorrect (currently expected != neovim state)
+ let editor = self.editor_state();
+ if neovim == marked_text && neovim == editor {
+ return;
+ }
+ let initial_state = self
+ .last_set_state
+ .as_ref()
+ .unwrap_or(&"N/A".to_string())
+ .clone();
+
+ let message = if neovim != marked_text {
+ "Test is incorrect (currently expected != neovim_state)"
+ } else {
+ "Editor does not match nvim behaviour"
+ };
+ panic!(
+ indoc! {"{}
# initial state:
{}
# keystrokes:
@@ -147,14 +156,59 @@ impl<'a> NeovimBackedTestContext<'a> {
{}
# zed state:
{}"},
- initial_state,
- self.recent_keystrokes.join(" "),
- marked_text,
- neovim,
- self.editor_state(),
- )
+ message,
+ initial_state,
+ self.recent_keystrokes.join(" "),
+ marked_text,
+ neovim,
+ editor
+ )
+ }
+
+ pub async fn assert_shared_clipboard(&mut self, text: &str) {
+ let neovim = self.neovim.read_register('"').await;
+ let editor = self
+ .platform()
+ .read_from_clipboard()
+ .unwrap()
+ .text()
+ .clone();
+
+ if text == neovim && text == editor {
+ return;
}
- self.assert_editor_state(marked_text)
+
+ let message = if neovim != text {
+ "Test is incorrect (currently expected != neovim)"
+ } else {
+ "Editor does not match nvim behaviour"
+ };
+
+ let initial_state = self
+ .last_set_state
+ .as_ref()
+ .unwrap_or(&"N/A".to_string())
+ .clone();
+
+ panic!(
+ indoc! {"{}
+ # initial state:
+ {}
+ # keystrokes:
+ {}
+ # currently expected:
+ {}
+ # neovim clipboard:
+ {}
+ # zed clipboard:
+ {}"},
+ message,
+ initial_state,
+ self.recent_keystrokes.join(" "),
+ text,
+ neovim,
+ editor
+ )
}
pub async fn neovim_state(&mut self) -> String {
@@ -40,6 +40,7 @@ pub enum NeovimData {
Put { state: String },
Key(String),
Get { state: String, mode: Option<Mode> },
+ ReadRegister { name: char, value: String },
}
pub struct NeovimConnection {
@@ -221,6 +222,36 @@ impl NeovimConnection {
);
}
+ #[cfg(not(feature = "neovim"))]
+ pub async fn read_register(&mut self, register: char) -> String {
+ if let Some(NeovimData::Get { .. }) = self.data.front() {
+ self.data.pop_front();
+ };
+ if let Some(NeovimData::ReadRegister { name, value }) = self.data.pop_front() {
+ if name == register {
+ return value;
+ }
+ }
+
+ panic!("operation does not match recorded script. re-record with --features=neovim")
+ }
+
+ #[cfg(feature = "neovim")]
+ pub async fn read_register(&mut self, name: char) -> String {
+ let value = self
+ .nvim
+ .command_output(format!("echo getreg('{}')", name).as_str())
+ .await
+ .unwrap();
+
+ self.data.push_back(NeovimData::ReadRegister {
+ name,
+ value: value.clone(),
+ });
+
+ value
+ }
+
#[cfg(feature = "neovim")]
async fn read_position(&mut self, cmd: &str) -> u32 {
self.nvim
@@ -7,10 +7,16 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut App
let mut text = String::new();
let mut clipboard_selections = Vec::with_capacity(selections.len());
{
+ let mut is_first = true;
for selection in selections.iter() {
- let initial_len = text.len();
let start = selection.start;
let end = selection.end;
+ if is_first {
+ is_first = false;
+ } else {
+ text.push_str("\n");
+ }
+ let initial_len = text.len();
for chunk in buffer.text_for_range(start..end) {
text.push_str(chunk);
}
@@ -1,14 +1,14 @@
-use std::{borrow::Cow, cmp, sync::Arc};
+use std::{cmp, sync::Arc};
use collections::HashMap;
use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
movement,
scroll::autoscroll::Autoscroll,
- Bias, ClipboardSelection, DisplayPoint, Editor,
+ Bias, DisplayPoint, Editor,
};
use gpui::{actions, AppContext, ViewContext, WindowContext};
-use language::{AutoindentMode, Selection, SelectionGoal};
+use language::{Selection, SelectionGoal};
use workspace::Workspace;
use crate::{
@@ -27,7 +27,6 @@ actions!(
ToggleVisualBlock,
VisualDelete,
VisualYank,
- VisualPaste,
OtherEnd,
]
);
@@ -47,7 +46,6 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(other_end);
cx.add_action(delete);
cx.add_action(yank);
- cx.add_action(paste);
}
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
@@ -331,110 +329,6 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
});
}
-pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>) {
- Vim::update(cx, |vim, cx| {
- vim.update_active_editor(cx, |editor, cx| {
- editor.transact(cx, |editor, cx| {
- if let Some(item) = cx.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::<Vec<ClipboardSelection>>()
- {
- 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 adjusted = selection.end;
- // 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;
- new_position.column = 0;
- new_position.row += 1;
- new_position
- } else {
- range.start
- };
-
- new_selections.push(selection.map(|_| new_position));
-
- 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(edits, Some(AutoindentMode::EachLine), cx);
- });
-
- editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
- s.select(new_selections)
- });
- } else {
- editor.insert(&clipboard_text, cx);
- }
- }
- });
- });
- vim.switch_mode(Mode::Normal, true, cx);
- });
-}
-
pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
@@ -796,65 +690,6 @@ mod test {
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 «jumpsˇ» over
- the lazy dog"},
- Mode::Visual,
- );
- 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 jumpsjumpˇs over
- the lazy dog"},
- Mode::Normal,
- );
-
- cx.set_state(
- indoc! {"
- The quick brown
- fox ju«mˇ»ps over
- the lazy dog"},
- Mode::VisualLine,
- );
- 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 «lazyˇ» dog"},
- Mode::Visual,
- );
- cx.simulate_keystroke("p");
- cx.assert_state(
- &indoc! {"
- The quick brown
- the_
- ˇfox jumps over
- dog"}
- .replace("_", " "), // Hack for trailing whitespace
- Mode::Normal,
- );
- }
-
#[gpui::test]
async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
@@ -1,13 +0,0 @@
-{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
-{"Key":"d"}
-{"Key":"d"}
-{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
-{"Key":"p"}
-{"Get":{"state":"The quick brown\nthe lazy dog\nˇfox jumps over","mode":"Normal"}}
-{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
-{"Key":"v"}
-{"Key":"w"}
-{"Key":"y"}
-{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
-{"Key":"p"}
-{"Get":{"state":"The quick brown\nfox jumps overjumps ˇo\nthe lazy dog","mode":"Normal"}}
@@ -0,0 +1,31 @@
+{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"jumps o"}}
+{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jumps overjumps ˇo\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
+{"Key":"shift-p"}
+{"Get":{"state":"The quick brown\nfox jumps ovejumps ˇor\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"d"}
+{"Key":"d"}
+{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nthe lazy dog\nˇfox jumps over","mode":"Normal"}}
+{"Key":"k"}
+{"Key":"shift-p"}
+{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog\nfox jumps over","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"j"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"over\nthe lazy do"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jumps oˇover\nthe lazy dover\nthe lazy dog","mode":"Normal"}}
+{"Key":"u"}
+{"Key":"shift-p"}
+{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy doover\nthe lazy dog","mode":"Normal"}}
@@ -0,0 +1,42 @@
+{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"y"}
+{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"w"}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jumps jumpˇs\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"over"}}
+{"Key":"up"}
+{"Key":"shift-v"}
+{"Key":"shift-p"}
+{"Get":{"state":"ˇover\nfox jumps jumps\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"over"}}
+{"Key":"ctrl-v"}
+{"Key":"down"}
+{"Key":"down"}
+{"Key":"p"}
+{"Get":{"state":"oveˇrver\noverox jumps jumps\noverhe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nthe \nˇfox jumps over\n dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"lazy"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"k"}
+{"Key":"shift-v"}
+{"Key":"p"}
+{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"The quick brown\n"}}
@@ -0,0 +1,31 @@
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"ctrl-v"}
+{"Key":"2"}
+{"Key":"j"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"q\nj\nl"}}
+{"Key":"p"}
+{"Get":{"state":"The qˇquick brown\nfox jjumps over\nthe llazy dog","mode":"Normal"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"shift-p"}
+{"Get":{"state":"The ˇq brown\nfox jjjumps over\nthe lllazy dog","mode":"Normal"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"shift-p"}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"ctrl-v"}
+{"Key":"j"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"q\nj"}}
+{"Key":"l"}
+{"Key":"ctrl-v"}
+{"Key":"2"}
+{"Key":"j"}
+{"Key":"shift-p"}
+{"Get":{"state":"The qˇqick brown\nfox jjmps over\nthe lzy dog","mode":"Normal"}}
+{"Key":"shift-v"}
+{"Key":"p"}
+{"Get":{"state":"ˇq\nj\nfox jjmps over\nthe lzy dog","mode":"Normal"}}
@@ -0,0 +1,26 @@
+{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"y"}
+{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jjumpˇsumps over\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nthe \nˇfox jumps over\n dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"lazy"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"k"}
+{"Key":"shift-v"}
+{"Key":"p"}
+{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}}