@@ -2,10 +2,6 @@
{
"context": "Editor && VimControl",
"bindings": {
- "i": [
- "vim::SwitchMode",
- "Insert"
- ],
"g": [
"vim::PushOperator",
{
@@ -13,6 +9,7 @@
}
],
"h": "vim::Left",
+ "backspace": "vim::Left",
"j": "vim::Down",
"k": "vim::Up",
"l": "vim::Right",
@@ -46,29 +43,39 @@
]
}
},
- {
- "context": "Editor && vim_operator == g",
- "bindings": {
- "g": "vim::StartOfDocument"
- }
- },
- {
- "context": "Editor && vim_mode == insert",
- "bindings": {
- "escape": "vim::NormalBefore",
- "ctrl-c": "vim::NormalBefore"
- }
- },
{
"context": "Editor && vim_mode == normal",
"bindings": {
+ "escape": "editor::Cancel",
"c": [
"vim::PushOperator",
"Change"
],
+ "shift-C": "vim::ChangeToEndOfLine",
"d": [
"vim::PushOperator",
"Delete"
+ ],
+ "shift-D": "vim::DeleteToEndOfLine",
+ "i": [
+ "vim::SwitchMode",
+ "Insert"
+ ],
+ "shift-I": "vim::InsertFirstNonWhitespace",
+ "a": "vim::InsertAfter",
+ "shift-A": "vim::InsertEndOfLine",
+ "x": "vim::DeleteRight",
+ "shift-X": "vim::DeleteLeft",
+ "shift-^": "vim::FirstNonWhitespace"
+ }
+ },
+ {
+ "context": "Editor && vim_operator == g",
+ "bindings": {
+ "g": "vim::StartOfDocument",
+ "escape": [
+ "vim::SwitchMode",
+ "Normal"
]
}
},
@@ -81,7 +88,27 @@
{
"ignorePunctuation": true
}
- ]
+ ],
+ "c": "vim::CurrentLine"
+ }
+ },
+ {
+ "context": "Editor && vim_operator == d",
+ "bindings": {
+ "d": "vim::CurrentLine"
+ }
+ },
+ {
+ "context": "Editor && vim_mode == insert",
+ "bindings": {
+ "escape": "vim::NormalBefore",
+ "ctrl-c": "vim::NormalBefore"
+ }
+ },
+ {
+ "context": "Editor && mode == singleline",
+ "bindings": {
+ "escape": "editor::Cancel"
}
}
]
@@ -1,7 +1,7 @@
use editor::{
char_kind,
display_map::{DisplaySnapshot, ToDisplayPoint},
- movement, Bias, DisplayPoint,
+ movement, Bias, CharKind, DisplayPoint,
};
use gpui::{actions, impl_actions, MutableAppContext};
use language::{Selection, SelectionGoal};
@@ -23,6 +23,8 @@ pub enum Motion {
NextWordStart { ignore_punctuation: bool },
NextWordEnd { ignore_punctuation: bool },
PreviousWordStart { ignore_punctuation: bool },
+ FirstNonWhitespace,
+ CurrentLine,
StartOfLine,
EndOfLine,
StartOfDocument,
@@ -57,8 +59,10 @@ actions!(
Down,
Up,
Right,
+ FirstNonWhitespace,
StartOfLine,
EndOfLine,
+ CurrentLine,
StartOfDocument,
EndOfDocument
]
@@ -70,8 +74,12 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
+ cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| {
+ motion(Motion::FirstNonWhitespace, cx)
+ });
cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
+ cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
motion(Motion::StartOfDocument, cx)
});
@@ -114,7 +122,7 @@ impl Motion {
pub fn linewise(self) -> bool {
use Motion::*;
match self {
- Down | Up | StartOfDocument | EndOfDocument => true,
+ Down | Up | StartOfDocument | EndOfDocument | CurrentLine => true,
_ => false,
}
}
@@ -156,8 +164,10 @@ impl Motion {
previous_word_start(map, point, ignore_punctuation),
SelectionGoal::None,
),
+ FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
StartOfLine => (start_of_line(map, point), SelectionGoal::None),
EndOfLine => (end_of_line(map, point), SelectionGoal::None),
+ CurrentLine => (end_of_line(map, point), SelectionGoal::None),
StartOfDocument => (start_of_document(map, point), SelectionGoal::None),
EndOfDocument => (end_of_document(map, point), SelectionGoal::None),
}
@@ -290,6 +300,24 @@ fn previous_word_start(
point
}
+fn first_non_whitespace(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+ let mut column = 0;
+ for ch in map.chars_at(DisplayPoint::new(point.row(), 0)) {
+ if ch == '\n' {
+ return point;
+ }
+
+ if char_kind(ch) != CharKind::Whitespace {
+ break;
+ }
+
+ column += ch.len_utf8() as u32;
+ }
+
+ *point.column_mut() = column;
+ map.clip_point(point, Bias::Left)
+}
+
fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
map.prev_line_boundary(point.to_point(map)).1
}
@@ -1,15 +1,60 @@
mod change;
mod delete;
-use crate::{motion::Motion, state::Operator, Vim};
+use crate::{
+ motion::Motion,
+ state::{Mode, Operator},
+ Vim,
+};
use change::init as change_init;
-use gpui::{actions, MutableAppContext};
+use gpui::{actions, MutableAppContext, ViewContext};
+use language::SelectionGoal;
+use workspace::Workspace;
use self::{change::change_over, delete::delete_over};
-actions!(vim, [InsertLineAbove, InsertLineBelow, InsertAfter]);
+actions!(
+ vim,
+ [
+ InsertAfter,
+ InsertFirstNonWhitespace,
+ InsertEndOfLine,
+ InsertLineAbove,
+ InsertLineBelow,
+ DeleteLeft,
+ DeleteRight,
+ ChangeToEndOfLine,
+ DeleteToEndOfLine,
+ ]
+);
pub fn init(cx: &mut MutableAppContext) {
+ cx.add_action(insert_after);
+ cx.add_action(insert_first_non_whitespace);
+ cx.add_action(insert_end_of_line);
+ cx.add_action(insert_line_above);
+ cx.add_action(insert_line_below);
+ cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
+ Vim::update(cx, |vim, cx| {
+ delete_over(vim, Motion::Left, cx);
+ })
+ });
+ cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
+ Vim::update(cx, |vim, cx| {
+ delete_over(vim, Motion::Right, cx);
+ })
+ });
+ cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
+ Vim::update(cx, |vim, cx| {
+ change_over(vim, Motion::EndOfLine, cx);
+ })
+ });
+ cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
+ Vim::update(cx, |vim, cx| {
+ delete_over(vim, Motion::EndOfLine, cx);
+ })
+ });
+
change_init(cx);
}
@@ -33,6 +78,88 @@ fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
});
}
+fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
+ Vim::update(cx, |vim, cx| {
+ vim.switch_mode(Mode::Insert, cx);
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.move_cursors(cx, |map, cursor, goal| {
+ Motion::Right.move_point(map, cursor, goal)
+ });
+ });
+ });
+}
+
+fn insert_first_non_whitespace(
+ _: &mut Workspace,
+ _: &InsertFirstNonWhitespace,
+ cx: &mut ViewContext<Workspace>,
+) {
+ Vim::update(cx, |vim, cx| {
+ vim.switch_mode(Mode::Insert, cx);
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.move_cursors(cx, |map, cursor, goal| {
+ Motion::FirstNonWhitespace.move_point(map, cursor, goal)
+ });
+ });
+ });
+}
+
+fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
+ Vim::update(cx, |vim, cx| {
+ vim.switch_mode(Mode::Insert, cx);
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.move_cursors(cx, |map, cursor, goal| {
+ Motion::EndOfLine.move_point(map, cursor, goal)
+ });
+ });
+ });
+}
+
+fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
+ Vim::update(cx, |vim, cx| {
+ vim.switch_mode(Mode::Insert, cx);
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ editor.move_cursors(cx, |map, cursor, goal| {
+ let (indent, _) = map.line_indent(cursor.row());
+ let (cursor, _) = Motion::EndOfLine.move_point(map, cursor, goal);
+ (cursor, SelectionGoal::Column(indent))
+ });
+ editor.insert("\n", cx);
+ editor.move_cursors(cx, |_, mut cursor, goal| {
+ if let SelectionGoal::Column(column) = goal {
+ *cursor.column_mut() = column;
+ }
+ (cursor, SelectionGoal::None)
+ });
+ });
+ });
+ });
+}
+
+fn insert_line_below(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
+ Vim::update(cx, |vim, cx| {
+ vim.switch_mode(Mode::Insert, cx);
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ editor.move_cursors(cx, |map, cursor, goal| {
+ let (indent, _) = map.line_indent(cursor.row());
+ let (cursor, _) = Motion::StartOfLine.move_point(map, cursor, goal);
+ (cursor, SelectionGoal::Column(indent))
+ });
+ editor.insert("\n", cx);
+ editor.move_cursors(cx, |_, mut cursor, goal| {
+ *cursor.row_mut() -= 1;
+ if let SelectionGoal::Column(column) = goal {
+ *cursor.column_mut() = column;
+ }
+ (cursor, SelectionGoal::None)
+ });
+ });
+ });
+ });
+}
+
#[cfg(test)]
mod test {
use indoc::indoc;
@@ -63,18 +190,18 @@ mod test {
}
#[gpui::test]
- async fn test_l(cx: &mut gpui::TestAppContext) {
+ async fn test_backspace(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
- let mut cx = cx.binding(["l"]);
- cx.assert("The q|uick", "The qu|ick");
- cx.assert("The quic|k", "The quic|k");
+ let mut cx = cx.binding(["backspace"]);
+ cx.assert("The q|uick", "The |quick");
+ cx.assert("|The quick", "|The quick");
cx.assert(
indoc! {"
- The quic|k
- brown"},
+ The quick
+ |brown"},
indoc! {"
- The quic|k
- brown"},
+ The quick
+ |brown"},
);
}
@@ -146,6 +273,22 @@ mod test {
);
}
+ #[gpui::test]
+ async fn test_l(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["l"]);
+ cx.assert("The q|uick", "The qu|ick");
+ cx.assert("The quic|k", "The quic|k");
+ cx.assert(
+ indoc! {"
+ The quic|k
+ brown"},
+ indoc! {"
+ The quic|k
+ brown"},
+ );
+ }
+
#[gpui::test]
async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
@@ -242,7 +385,7 @@ mod test {
}
#[gpui::test]
- async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
+ async fn test_w(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
let (_, cursor_offsets) = marked_text(indoc! {"
The |quick|-|brown
@@ -289,7 +432,7 @@ mod test {
}
#[gpui::test]
- async fn test_next_word_end(cx: &mut gpui::TestAppContext) {
+ async fn test_e(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
let (_, cursor_offsets) = marked_text(indoc! {"
Th|e quic|k|-brow|n
@@ -335,7 +478,7 @@ mod test {
}
#[gpui::test]
- async fn test_previous_word_start(cx: &mut gpui::TestAppContext) {
+ async fn test_b(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
let (_, cursor_offsets) = marked_text(indoc! {"
||The |quick|-|brown
@@ -397,7 +540,7 @@ mod test {
}
#[gpui::test]
- async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
+ async fn test_gg(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["g", "g"]);
cx.assert(
@@ -449,4 +592,418 @@ mod test {
over the lazy dog"},
);
}
+
+ #[gpui::test]
+ async fn test_a(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["a"]).mode_after(Mode::Insert);
+
+ cx.assert("The q|uick", "The qu|ick");
+ cx.assert("The quic|k", "The quick|");
+ }
+
+ #[gpui::test]
+ async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["shift-A"]).mode_after(Mode::Insert);
+ cx.assert("The q|uick", "The quick|");
+ cx.assert("The q|uick ", "The quick |");
+ cx.assert("|", "|");
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox"},
+ indoc! {"
+ The quick|
+ brown fox"},
+ );
+ cx.assert(
+ indoc! {"
+ |
+ The quick"},
+ indoc! {"
+ |
+ The quick"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["shift-^"]);
+ cx.assert("The q|uick", "|The quick");
+ cx.assert(" The q|uick", " |The quick");
+ cx.assert("|", "|");
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox"},
+ indoc! {"
+ |The quick
+ brown fox"},
+ );
+ cx.assert(
+ indoc! {"
+ |
+ The quick"},
+ indoc! {"
+ |
+ The quick"},
+ );
+ cx.assert(
+ indoc! {"
+ |
+ The quick"},
+ indoc! {"
+ |
+ The quick"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["shift-I"]).mode_after(Mode::Insert);
+ cx.assert("The q|uick", "|The quick");
+ cx.assert(" The q|uick", " |The quick");
+ cx.assert("|", "|");
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox"},
+ indoc! {"
+ |The quick
+ brown fox"},
+ );
+ cx.assert(
+ indoc! {"
+ |
+ The quick"},
+ indoc! {"
+ |
+ The quick"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["shift-D"]);
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox"},
+ indoc! {"
+ The |q
+ brown fox"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_x(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["x"]);
+ cx.assert("|Test", "|est");
+ cx.assert("Te|st", "Te|t");
+ cx.assert("Tes|t", "Te|s");
+ cx.assert(
+ indoc! {"
+ Tes|t
+ test"},
+ indoc! {"
+ Te|s
+ test"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_delete_left(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["shift-X"]);
+ cx.assert("Te|st", "T|st");
+ cx.assert("T|est", "|est");
+ cx.assert("|Test", "|Test");
+ cx.assert(
+ indoc! {"
+ Test
+ |test"},
+ indoc! {"
+ Test
+ |test"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_o(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["o"]).mode_after(Mode::Insert);
+
+ cx.assert(
+ "|",
+ indoc! {"
+
+ |"},
+ );
+ cx.assert(
+ "The |quick",
+ indoc! {"
+ The quick
+ |"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown |fox
+ jumps over"},
+ indoc! {"
+ The quick
+ brown fox
+ |
+ jumps over"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps |over"},
+ indoc! {"
+ The quick
+ brown fox
+ jumps over
+ |"},
+ );
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over"},
+ indoc! {"
+ The quick
+ |
+ brown fox
+ jumps over"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ indoc! {"
+ The quick
+
+ |
+ brown fox"},
+ );
+ cx.assert(
+ indoc! {"
+ fn test() {
+ println!(|);
+ }"},
+ indoc! {"
+ fn test() {
+ println!();
+ |
+ }"},
+ );
+ cx.assert(
+ indoc! {"
+ fn test(|) {
+ println!();
+ }"},
+ indoc! {"
+ fn test() {
+ |
+ println!();
+ }"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["shift-O"]).mode_after(Mode::Insert);
+
+ cx.assert(
+ "|",
+ indoc! {"
+ |
+ "},
+ );
+ cx.assert(
+ "The |quick",
+ indoc! {"
+ |
+ The quick"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown |fox
+ jumps over"},
+ indoc! {"
+ The quick
+ |
+ brown fox
+ jumps over"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps |over"},
+ indoc! {"
+ The quick
+ brown fox
+ |
+ jumps over"},
+ );
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over"},
+ indoc! {"
+ |
+ The quick
+ brown fox
+ jumps over"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ indoc! {"
+ The quick
+ |
+
+ brown fox"},
+ );
+ cx.assert(
+ indoc! {"
+ fn test() {
+ println!(|);
+ }"},
+ indoc! {"
+ fn test() {
+ |
+ println!();
+ }"},
+ );
+ cx.assert(
+ indoc! {"
+ fn test(|) {
+ println!();
+ }"},
+ indoc! {"
+ |
+ fn test() {
+ println!();
+ }"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_dd(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["d", "d"]);
+
+ cx.assert("|", "|");
+ cx.assert("The |quick", "|");
+ cx.assert(
+ indoc! {"
+ The quick
+ brown |fox
+ jumps over"},
+ indoc! {"
+ The quick
+ jumps |over"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps |over"},
+ indoc! {"
+ The quick
+ brown |fox"},
+ );
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over"},
+ indoc! {"
+ brown| fox
+ jumps over"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ indoc! {"
+ The quick
+ |brown fox"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_cc(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["c", "c"]).mode_after(Mode::Insert);
+
+ cx.assert("|", "|");
+ cx.assert("The |quick", "|");
+ cx.assert(
+ indoc! {"
+ The quick
+ brown |fox
+ jumps over"},
+ indoc! {"
+ The quick
+ |
+ jumps over"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps |over"},
+ indoc! {"
+ The quick
+ brown fox
+ |"},
+ );
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over"},
+ indoc! {"
+ |
+ brown fox
+ jumps over"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ );
+ }
}