Merge pull request #947 from zed-industries/misc-normal-commands

Misc vim normal commands
This commit is contained in:
Keith Simmons 2022-05-03 10:29:29 -07:00 committed by GitHub
commit 5e2e859413
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 700 additions and 42 deletions

View File

@ -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,41 @@
]
}
},
{
"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",
"o": "vim::InsertLineBelow",
"shift-O": "vim::InsertLineAbove"
}
},
{
"context": "Editor && vim_operator == g",
"bindings": {
"g": "vim::StartOfDocument",
"escape": [
"vim::SwitchMode",
"Normal"
]
}
},
@ -81,7 +90,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"
}
}
]

View File

@ -1334,6 +1334,19 @@ impl Editor {
self.update_selections(vec![selection], None, cx);
}
pub fn display_selections(
&mut self,
cx: &mut ViewContext<Self>,
) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self
.local_selections::<Point>(cx)
.into_iter()
.map(|selection| selection.map(|point| point.to_display_point(&display_map)))
.collect();
(display_map, selections)
}
pub fn move_selections(
&mut self,
cx: &mut ViewContext<Self>,
@ -1382,6 +1395,25 @@ impl Editor {
});
}
pub fn edit<I, S, T>(&mut self, edits: I, cx: &mut ViewContext<Self>)
where
I: IntoIterator<Item = (Range<S>, T)>,
S: ToOffset,
T: Into<Arc<str>>,
{
self.buffer.update(cx, |buffer, cx| buffer.edit(edits, cx));
}
pub fn edit_with_autoindent<I, S, T>(&mut self, edits: I, cx: &mut ViewContext<Self>)
where
I: IntoIterator<Item = (Range<S>, T)>,
S: ToOffset,
T: Into<Arc<str>>,
{
self.buffer
.update(cx, |buffer, cx| buffer.edit_with_autoindent(edits, cx));
}
fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext<Self>) {
self.hide_context_menu(cx);
@ -1456,6 +1488,7 @@ impl Editor {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let newest_selection = self.newest_anchor_selection().clone();
let position = display_map.clip_point(position, Bias::Left);
let start;
let end;

View File

@ -18,15 +18,11 @@ fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppCont
}
fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) {
let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) {
Mode::Insert
} else {
Mode::Normal
};
Vim::update(cx, |state, cx| {
state.active_editor = Some(editor.downgrade());
state.switch_mode(mode, cx);
if editor.read(cx).mode() != EditorMode::Full {
state.switch_mode(Mode::Insert, cx);
}
});
}

View File

@ -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
}

View File

@ -1,15 +1,62 @@
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 collections::HashSet;
use editor::{Bias, DisplayPoint};
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 +80,101 @@ 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| {
let (map, old_selections) = editor.display_selections(cx);
let selection_start_rows: HashSet<u32> = old_selections
.into_iter()
.map(|selection| selection.start.row())
.collect();
let edits = selection_start_rows.into_iter().map(|row| {
let (indent, _) = map.line_indent(row);
let start_of_line = map
.clip_point(DisplayPoint::new(row, 0), Bias::Left)
.to_point(&map);
let mut new_text = " ".repeat(indent as usize);
new_text.push('\n');
(start_of_line..start_of_line, new_text)
});
editor.edit(edits, cx);
editor.move_cursors(cx, |map, mut cursor, _| {
*cursor.row_mut() -= 1;
*cursor.column_mut() = map.line_len(cursor.row());
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
});
});
});
});
}
fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, 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| {
let (map, old_selections) = editor.display_selections(cx);
let selection_end_rows: HashSet<u32> = old_selections
.into_iter()
.map(|selection| selection.end.row())
.collect();
let edits = selection_end_rows.into_iter().map(|row| {
let (indent, _) = map.line_indent(row);
let end_of_line = map
.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left)
.to_point(&map);
let mut new_text = "\n".to_string();
new_text.push_str(&" ".repeat(indent as usize));
(end_of_line..end_of_line, new_text)
});
editor.move_cursors(cx, |map, cursor, goal| {
Motion::EndOfLine.move_point(map, cursor, goal)
});
editor.edit(edits, cx);
});
});
});
}
#[cfg(test)]
mod test {
use indoc::indoc;
@ -63,18 +205,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 +288,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 +400,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 +447,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 +493,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 +555,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 +607,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"},
);
}
}