mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
Rewrite paste
- vim: support P for paste before - vim: support P in visual mode for paste without overriding clipboard - vim: fix position when using `p` on text copied outside zed - vim: fix indentation when using `p` on text copied from zed
This commit is contained in:
parent
31db5e4f62
commit
33d7fe02ee
@ -287,6 +287,12 @@
|
|||||||
"shift-o": "vim::InsertLineAbove",
|
"shift-o": "vim::InsertLineAbove",
|
||||||
"~": "vim::ChangeCase",
|
"~": "vim::ChangeCase",
|
||||||
"p": "vim::Paste",
|
"p": "vim::Paste",
|
||||||
|
"shift-p": [
|
||||||
|
"vim::Paste",
|
||||||
|
{
|
||||||
|
"before": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"u": "editor::Undo",
|
"u": "editor::Undo",
|
||||||
"ctrl-r": "editor::Redo",
|
"ctrl-r": "editor::Redo",
|
||||||
"/": "vim::Search",
|
"/": "vim::Search",
|
||||||
@ -375,7 +381,13 @@
|
|||||||
"d": "vim::VisualDelete",
|
"d": "vim::VisualDelete",
|
||||||
"x": "vim::VisualDelete",
|
"x": "vim::VisualDelete",
|
||||||
"y": "vim::VisualYank",
|
"y": "vim::VisualYank",
|
||||||
"p": "vim::VisualPaste",
|
"p": "vim::Paste",
|
||||||
|
"shift-p": [
|
||||||
|
"vim::Paste",
|
||||||
|
{
|
||||||
|
"preserveClipboard": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"s": "vim::Substitute",
|
"s": "vim::Substitute",
|
||||||
"c": "vim::Substitute",
|
"c": "vim::Substitute",
|
||||||
"~": "vim::ChangeCase",
|
"~": "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>) {
|
fn select(&mut self, phase: SelectPhase, cx: &mut ViewContext<Self>) {
|
||||||
self.hide_context_menu(cx);
|
self.hide_context_menu(cx);
|
||||||
|
|
||||||
|
@ -162,6 +162,15 @@ impl<'a> EditorLspTestContext<'a> {
|
|||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
name: "Typescript".into(),
|
name: "Typescript".into(),
|
||||||
path_suffixes: vec!["ts".to_string()],
|
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,
|
word_characters,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@ -174,6 +183,23 @@ impl<'a> EditorLspTestContext<'a> {
|
|||||||
("{" @open "}" @close)
|
("{" @open "}" @close)
|
||||||
("<" @open ">" @close)
|
("<" @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()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.expect("Could not parse queries");
|
.expect("Could not parse queries");
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
mod case;
|
mod case;
|
||||||
mod change;
|
mod change;
|
||||||
mod delete;
|
mod delete;
|
||||||
|
mod paste;
|
||||||
mod scroll;
|
mod scroll;
|
||||||
mod search;
|
mod search;
|
||||||
pub mod substitute;
|
pub mod substitute;
|
||||||
mod yank;
|
mod yank;
|
||||||
|
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
motion::Motion,
|
motion::Motion,
|
||||||
@ -14,13 +15,11 @@ use crate::{
|
|||||||
state::{Mode, Operator},
|
state::{Mode, Operator},
|
||||||
Vim,
|
Vim,
|
||||||
};
|
};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::HashSet;
|
||||||
use editor::{
|
use editor::scroll::autoscroll::Autoscroll;
|
||||||
display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, Bias, ClipboardSelection,
|
use editor::{Bias, DisplayPoint};
|
||||||
DisplayPoint,
|
|
||||||
};
|
|
||||||
use gpui::{actions, AppContext, ViewContext, WindowContext};
|
use gpui::{actions, AppContext, ViewContext, WindowContext};
|
||||||
use language::{AutoindentMode, Point, SelectionGoal};
|
use language::SelectionGoal;
|
||||||
use log::error;
|
use log::error;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
@ -44,7 +43,6 @@ actions!(
|
|||||||
DeleteRight,
|
DeleteRight,
|
||||||
ChangeToEndOfLine,
|
ChangeToEndOfLine,
|
||||||
DeleteToEndOfLine,
|
DeleteToEndOfLine,
|
||||||
Paste,
|
|
||||||
Yank,
|
Yank,
|
||||||
Substitute,
|
Substitute,
|
||||||
ChangeCase,
|
ChangeCase,
|
||||||
@ -89,9 +87,8 @@ pub fn init(cx: &mut AppContext) {
|
|||||||
delete_motion(vim, Motion::EndOfLine, times, cx);
|
delete_motion(vim, Motion::EndOfLine, times, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
cx.add_action(paste);
|
|
||||||
|
|
||||||
scroll::init(cx);
|
scroll::init(cx);
|
||||||
|
paste::init(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn normal_motion(
|
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) {
|
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
@ -883,36 +742,6 @@ mod test {
|
|||||||
.await;
|
.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]
|
#[gpui::test]
|
||||||
async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
|
async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
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) {
|
pub async fn assert_shared_state(&mut self, marked_text: &str) {
|
||||||
let neovim = self.neovim_state().await;
|
let neovim = self.neovim_state().await;
|
||||||
if neovim != marked_text {
|
let editor = self.editor_state();
|
||||||
let initial_state = self
|
if neovim == marked_text && neovim == editor {
|
||||||
.last_set_state
|
return;
|
||||||
.as_ref()
|
}
|
||||||
.unwrap_or(&"N/A".to_string())
|
let initial_state = self
|
||||||
.clone();
|
.last_set_state
|
||||||
panic!(
|
.as_ref()
|
||||||
indoc! {"Test is incorrect (currently expected != neovim state)
|
.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:
|
# initial state:
|
||||||
{}
|
{}
|
||||||
# keystrokes:
|
# keystrokes:
|
||||||
@ -147,14 +156,59 @@ impl<'a> NeovimBackedTestContext<'a> {
|
|||||||
{}
|
{}
|
||||||
# zed state:
|
# zed state:
|
||||||
{}"},
|
{}"},
|
||||||
initial_state,
|
message,
|
||||||
self.recent_keystrokes.join(" "),
|
initial_state,
|
||||||
marked_text,
|
self.recent_keystrokes.join(" "),
|
||||||
neovim,
|
marked_text,
|
||||||
self.editor_state(),
|
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 {
|
pub async fn neovim_state(&mut self) -> String {
|
||||||
|
@ -40,6 +40,7 @@ pub enum NeovimData {
|
|||||||
Put { state: String },
|
Put { state: String },
|
||||||
Key(String),
|
Key(String),
|
||||||
Get { state: String, mode: Option<Mode> },
|
Get { state: String, mode: Option<Mode> },
|
||||||
|
ReadRegister { name: char, value: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NeovimConnection {
|
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")]
|
#[cfg(feature = "neovim")]
|
||||||
async fn read_position(&mut self, cmd: &str) -> u32 {
|
async fn read_position(&mut self, cmd: &str) -> u32 {
|
||||||
self.nvim
|
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 text = String::new();
|
||||||
let mut clipboard_selections = Vec::with_capacity(selections.len());
|
let mut clipboard_selections = Vec::with_capacity(selections.len());
|
||||||
{
|
{
|
||||||
|
let mut is_first = true;
|
||||||
for selection in selections.iter() {
|
for selection in selections.iter() {
|
||||||
let initial_len = text.len();
|
|
||||||
let start = selection.start;
|
let start = selection.start;
|
||||||
let end = selection.end;
|
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) {
|
for chunk in buffer.text_for_range(start..end) {
|
||||||
text.push_str(chunk);
|
text.push_str(chunk);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
use std::{borrow::Cow, cmp, sync::Arc};
|
use std::{cmp, sync::Arc};
|
||||||
|
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::{
|
use editor::{
|
||||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||||
movement,
|
movement,
|
||||||
scroll::autoscroll::Autoscroll,
|
scroll::autoscroll::Autoscroll,
|
||||||
Bias, ClipboardSelection, DisplayPoint, Editor,
|
Bias, DisplayPoint, Editor,
|
||||||
};
|
};
|
||||||
use gpui::{actions, AppContext, ViewContext, WindowContext};
|
use gpui::{actions, AppContext, ViewContext, WindowContext};
|
||||||
use language::{AutoindentMode, Selection, SelectionGoal};
|
use language::{Selection, SelectionGoal};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -27,7 +27,6 @@ actions!(
|
|||||||
ToggleVisualBlock,
|
ToggleVisualBlock,
|
||||||
VisualDelete,
|
VisualDelete,
|
||||||
VisualYank,
|
VisualYank,
|
||||||
VisualPaste,
|
|
||||||
OtherEnd,
|
OtherEnd,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@ -47,7 +46,6 @@ pub fn init(cx: &mut AppContext) {
|
|||||||
cx.add_action(other_end);
|
cx.add_action(other_end);
|
||||||
cx.add_action(delete);
|
cx.add_action(delete);
|
||||||
cx.add_action(yank);
|
cx.add_action(yank);
|
||||||
cx.add_action(paste);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
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) {
|
pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
@ -796,65 +690,6 @@ mod test {
|
|||||||
fox jumps o"}));
|
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]
|
#[gpui::test]
|
||||||
async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
|
async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
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