Visual line mode handles soft wraps
This commit is contained in:
parent
33940b5dd9
commit
61f0daa5c5
14 changed files with 314 additions and 96 deletions
|
@ -193,11 +193,13 @@ impl Motion {
|
|||
if selection.end.row() < map.max_point().row() {
|
||||
*selection.end.row_mut() += 1;
|
||||
*selection.end.column_mut() = 0;
|
||||
selection.end = map.clip_point(selection.end, Bias::Right);
|
||||
// Don't reset the end here
|
||||
return;
|
||||
} else if selection.start.row() > 0 {
|
||||
*selection.start.row_mut() -= 1;
|
||||
*selection.start.column_mut() = map.line_len(selection.start.row());
|
||||
selection.start = map.clip_point(selection.start, Bias::Left);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
mod change;
|
||||
mod delete;
|
||||
mod yank;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
@ -15,7 +16,7 @@ use gpui::{actions, MutableAppContext, ViewContext};
|
|||
use language::{Point, SelectionGoal};
|
||||
use workspace::Workspace;
|
||||
|
||||
use self::{change::change_over, delete::delete_over};
|
||||
use self::{change::change_over, delete::delete_over, yank::yank_over};
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
|
@ -69,11 +70,12 @@ pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
|
|||
Vim::update(cx, |vim, cx| {
|
||||
match vim.state.operator_stack.pop() {
|
||||
None => move_cursor(vim, motion, cx),
|
||||
Some(Operator::Change) => change_over(vim, motion, cx),
|
||||
Some(Operator::Delete) => delete_over(vim, motion, cx),
|
||||
Some(Operator::Namespace(_)) => {
|
||||
// Can't do anything for a namespace operator. Ignoring
|
||||
}
|
||||
Some(Operator::Change) => change_over(vim, motion, cx),
|
||||
Some(Operator::Delete) => delete_over(vim, motion, cx),
|
||||
Some(Operator::Yank) => yank_over(vim, motion, cx),
|
||||
}
|
||||
vim.clear_operator(cx);
|
||||
});
|
||||
|
|
26
crates/vim/src/normal/yank.rs
Normal file
26
crates/vim/src/normal/yank.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use crate::{motion::Motion, utils::copy_selections_content, Vim};
|
||||
use collections::HashMap;
|
||||
use gpui::MutableAppContext;
|
||||
|
||||
pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_positions: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let original_position = (selection.head(), selection.goal);
|
||||
motion.expand_selection(map, selection, true);
|
||||
original_positions.insert(selection.id, original_position);
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, motion.linewise(), cx);
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|_, selection| {
|
||||
let (head, goal) = original_positions.remove(&selection.id).unwrap();
|
||||
selection.collapse_to(head, goal);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -26,6 +26,7 @@ pub enum Operator {
|
|||
Namespace(Namespace),
|
||||
Change,
|
||||
Delete,
|
||||
Yank,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -80,6 +81,7 @@ impl Operator {
|
|||
Operator::Namespace(Namespace::G) => "g",
|
||||
Operator::Change => "c",
|
||||
Operator::Delete => "d",
|
||||
Operator::Yank => "y",
|
||||
}
|
||||
.to_owned();
|
||||
|
||||
|
|
|
@ -42,8 +42,10 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
},
|
||||
);
|
||||
|
||||
cx.observe_global::<Settings, _>(|settings, cx| {
|
||||
Vim::update(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
|
||||
cx.observe_global::<Settings, _>(|cx| {
|
||||
Vim::update(cx, |state, cx| {
|
||||
state.set_enabled(cx.global::<Settings>().vim_mode, cx)
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
@ -141,14 +143,11 @@ impl Vim {
|
|||
}
|
||||
|
||||
if state.empty_selections_only() {
|
||||
// Defer so that access to global settings object doesn't panic
|
||||
cx.defer(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|_, selection| {
|
||||
selection.collapse_to(selection.head(), selection.goal)
|
||||
});
|
||||
})
|
||||
});
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|_, selection| {
|
||||
selection.collapse_to(selection.head(), selection.goal)
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -337,6 +337,14 @@ impl<'a> VimTestContext<'a> {
|
|||
let mode = self.mode();
|
||||
VimBindingTestContext::new(keystrokes, mode, mode, self)
|
||||
}
|
||||
|
||||
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
|
||||
self.cx.update(|cx| {
|
||||
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
|
||||
let expected_content = expected_content.map(|content| content.to_owned());
|
||||
assert_eq!(actual_content, expected_content);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for VimTestContext<'a> {
|
||||
|
|
|
@ -9,9 +9,11 @@ actions!(
|
|||
vim,
|
||||
[
|
||||
VisualDelete,
|
||||
VisualChange,
|
||||
VisualLineDelete,
|
||||
VisualLineChange
|
||||
VisualChange,
|
||||
VisualLineChange,
|
||||
VisualYank,
|
||||
VisualLineYank,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -20,6 +22,8 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
cx.add_action(change_line);
|
||||
cx.add_action(delete);
|
||||
cx.add_action(delete_line);
|
||||
cx.add_action(yank);
|
||||
cx.add_action(yank_line);
|
||||
}
|
||||
|
||||
pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||
|
@ -56,8 +60,8 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
|
|||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.reversed {
|
||||
// Head was at the end of the selection, and now is at the start. We need to move the end
|
||||
// forward by one if possible in order to compensate for this change.
|
||||
// Head is at the end of the selection. Adjust the end position to
|
||||
// to include the character under the cursor.
|
||||
*selection.end.column_mut() = selection.end.column() + 1;
|
||||
selection.end = map.clip_point(selection.end, Bias::Left);
|
||||
}
|
||||
|
@ -74,12 +78,9 @@ pub fn change_line(_: &mut Workspace, _: &VisualLineChange, cx: &mut ViewContext
|
|||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
|
||||
selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
|
||||
});
|
||||
});
|
||||
|
||||
let adjusted = editor.selections.all_adjusted(cx);
|
||||
editor.change_selections(None, cx, |s| s.select(adjusted));
|
||||
copy_selections_content(editor, true, cx);
|
||||
editor.insert("", cx);
|
||||
});
|
||||
|
@ -131,11 +132,13 @@ pub fn delete_line(_: &mut Workspace, _: &VisualLineDelete, cx: &mut ViewContext
|
|||
if selection.end.row() < map.max_point().row() {
|
||||
*selection.end.row_mut() += 1;
|
||||
*selection.end.column_mut() = 0;
|
||||
selection.end = map.clip_point(selection.end, Bias::Right);
|
||||
// Don't reset the end here
|
||||
return;
|
||||
} else if selection.start.row() > 0 {
|
||||
*selection.start.row_mut() -= 1;
|
||||
*selection.start.column_mut() = map.line_len(selection.start.row());
|
||||
selection.start = map.clip_point(selection.start, Bias::Left);
|
||||
}
|
||||
|
||||
selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
|
||||
|
@ -161,6 +164,38 @@ pub fn delete_line(_: &mut Workspace, _: &VisualLineDelete, cx: &mut ViewContext
|
|||
});
|
||||
}
|
||||
|
||||
pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.reversed {
|
||||
// Head is at the end of the selection. Adjust the end position to
|
||||
// to include the character under the cursor.
|
||||
*selection.end.column_mut() = selection.end.column() + 1;
|
||||
selection.end = map.clip_point(selection.end, Bias::Left);
|
||||
}
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, false, cx);
|
||||
});
|
||||
vim.switch_mode(Mode::Normal, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn yank_line(_: &mut Workspace, _: &VisualLineYank, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let adjusted = editor.selections.all_adjusted(cx);
|
||||
editor.change_selections(None, cx, |s| s.select(adjusted));
|
||||
copy_selections_content(editor, true, cx);
|
||||
});
|
||||
vim.switch_mode(Mode::Normal, cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
@ -521,4 +556,88 @@ mod test {
|
|||
|"},
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
|
||||
let cx = VimTestContext::new(cx, true).await;
|
||||
let mut cx = cx.binding(["v", "w", "y"]);
|
||||
cx.assert("The quick |brown", "The quick |brown");
|
||||
cx.assert_clipboard_content(Some("brown"));
|
||||
let mut cx = cx.binding(["v", "w", "j", "y"]);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The |quick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The |quick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
);
|
||||
cx.assert_clipboard_content(Some(indoc! {"
|
||||
quick brown
|
||||
fox jumps ov"}));
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the |lazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the |lazy dog"},
|
||||
);
|
||||
cx.assert_clipboard_content(Some("lazy d"));
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps |over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps |over
|
||||
the lazy dog"},
|
||||
);
|
||||
cx.assert_clipboard_content(Some(indoc! {"
|
||||
over
|
||||
t"}));
|
||||
let mut cx = cx.binding(["v", "b", "k", "y"]);
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The |quick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The |quick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
);
|
||||
cx.assert_clipboard_content(Some("The q"));
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the |lazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the |lazy dog"},
|
||||
);
|
||||
cx.assert_clipboard_content(Some(indoc! {"
|
||||
fox jumps over
|
||||
the l"}));
|
||||
cx.assert(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps |over
|
||||
the lazy dog"},
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps |over
|
||||
the lazy dog"},
|
||||
);
|
||||
cx.assert_clipboard_content(Some(indoc! {"
|
||||
quick brown
|
||||
fox jumps o"}));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue