mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-26 06:11:34 +03:00
vim: Rewrite paste (#2878)
A complete overhaul of the way vim did paste. This ended up being more involved than I expected because of the variety of different behaviors that vim exhibits when copying/pasting between various modes. Release Notes: - vim: support P for paste before ([#1869](https://github.com/zed-industries/community/issues/1869)). - vim: support P in visual modes for paste without overriding clipboard - vim: fix position when using `p` on text copied outside zed ([#469](https://github.com/zed-industries/community/issues/469)). - vim: fix indentation when using `p` on text copied from zed ([#1015](https://github.com/zed-industries/community/issues/1015)). - all: Separate copied multi-selections by `\n`
This commit is contained in:
commit
b0815bd13e
@ -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;
|
||||
|
468
crates/vim/src/normal/paste.rs
Normal file
468
crates/vim/src/normal/paste.rs
Normal file
@ -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"}}
|
31
crates/vim/test_data/test_paste.json
Normal file
31
crates/vim/test_data/test_paste.json
Normal file
@ -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"}}
|
42
crates/vim/test_data/test_paste_visual.json
Normal file
42
crates/vim/test_data/test_paste_visual.json
Normal file
@ -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"}}
|
31
crates/vim/test_data/test_paste_visual_block.json
Normal file
31
crates/vim/test_data/test_paste_visual_block.json
Normal file
@ -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"}}
|
26
crates/vim/test_data/test_visual_paste.json
Normal file
26
crates/vim/test_data/test_visual_paste.json
Normal file
@ -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"}}
|
Loading…
Reference in New Issue
Block a user