zed/crates/vim/src/normal.rs
Conrad Irwin c8bc49fa18
vim: Fix count in visual indent (#15296)
Co-Authored-By: tobbe@tlundberg.com

Release Notes:

- vim: Added {count} for `>`/`<` in visual mode
2024-07-26 16:03:41 -06:00

1497 lines
45 KiB
Rust

mod case;
mod change;
mod delete;
mod increment;
mod indent;
pub(crate) mod mark;
mod paste;
pub(crate) mod repeat;
mod scroll;
pub(crate) mod search;
pub mod substitute;
mod toggle_comments;
pub(crate) mod yank;
use std::collections::HashMap;
use std::sync::Arc;
use crate::{
motion::{self, first_non_whitespace, next_line_end, right, Motion},
object::Object,
state::{Mode, Operator},
surrounds::{check_and_move_to_valid_bracket_pair, SurroundsType},
Vim,
};
use case::{change_case_motion, change_case_object, CaseTarget};
use collections::BTreeSet;
use editor::scroll::Autoscroll;
use editor::Anchor;
use editor::Bias;
use editor::Editor;
use editor::{display_map::ToDisplayPoint, movement};
use gpui::{actions, ViewContext, WindowContext};
use language::{Point, SelectionGoal};
use log::error;
use multi_buffer::MultiBufferRow;
use workspace::Workspace;
use self::{
case::{change_case, convert_to_lower_case, convert_to_upper_case},
change::{change_motion, change_object},
delete::{delete_motion, delete_object},
indent::{indent_motion, indent_object, IndentDirection},
toggle_comments::{toggle_comments_motion, toggle_comments_object},
yank::{yank_motion, yank_object},
};
actions!(
vim,
[
InsertAfter,
InsertBefore,
InsertFirstNonWhitespace,
InsertEndOfLine,
InsertLineAbove,
InsertLineBelow,
InsertAtPrevious,
DeleteLeft,
DeleteRight,
ChangeToEndOfLine,
DeleteToEndOfLine,
Yank,
YankLine,
YankToEndOfLine,
ChangeCase,
ConvertToUpperCase,
ConvertToLowerCase,
JoinLines,
Indent,
Outdent,
ToggleComments,
Undo,
Redo,
]
);
pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
workspace.register_action(insert_after);
workspace.register_action(insert_before);
workspace.register_action(insert_first_non_whitespace);
workspace.register_action(insert_end_of_line);
workspace.register_action(insert_line_above);
workspace.register_action(insert_line_below);
workspace.register_action(insert_at_previous);
workspace.register_action(change_case);
workspace.register_action(convert_to_upper_case);
workspace.register_action(convert_to_lower_case);
workspace.register_action(yank_line);
workspace.register_action(yank_to_end_of_line);
workspace.register_action(toggle_comments);
workspace.register_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let times = vim.take_count(cx);
delete_motion(vim, Motion::Left, times, cx);
})
});
workspace.register_action(|_: &mut Workspace, _: &DeleteRight, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let times = vim.take_count(cx);
delete_motion(vim, Motion::Right, times, cx);
})
});
workspace.register_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
let times = vim.take_count(cx);
change_motion(
vim,
Motion::EndOfLine {
display_lines: false,
},
times,
cx,
);
})
});
workspace.register_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let times = vim.take_count(cx);
delete_motion(
vim,
Motion::EndOfLine {
display_lines: false,
},
times,
cx,
);
})
});
workspace.register_action(|_: &mut Workspace, _: &JoinLines, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let mut times = vim.take_count(cx).unwrap_or(1);
if vim.state().mode.is_visual() {
times = 1;
} else if times > 1 {
// 2J joins two lines together (same as J or 1J)
times -= 1;
}
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
for _ in 0..times {
editor.join_lines(&Default::default(), cx)
}
})
});
if vim.state().mode.is_visual() {
vim.switch_mode(Mode::Normal, false, cx)
}
});
});
workspace.register_action(|_: &mut Workspace, _: &Indent, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let count = vim.take_count(cx).unwrap_or(1);
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let mut original_positions = save_selection_starts(editor, cx);
for _ in 0..count {
editor.indent(&Default::default(), cx);
}
restore_selection_cursors(editor, cx, &mut original_positions);
});
});
if vim.state().mode.is_visual() {
vim.switch_mode(Mode::Normal, false, cx)
}
});
});
workspace.register_action(|_: &mut Workspace, _: &Outdent, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let count = vim.take_count(cx).unwrap_or(1);
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let mut original_positions = save_selection_starts(editor, cx);
for _ in 0..count {
editor.outdent(&Default::default(), cx);
}
restore_selection_cursors(editor, cx, &mut original_positions);
});
});
if vim.state().mode.is_visual() {
vim.switch_mode(Mode::Normal, false, cx)
}
});
});
workspace.register_action(|_: &mut Workspace, _: &Undo, cx| {
Vim::update(cx, |vim, cx| {
let times = vim.take_count(cx);
vim.update_active_editor(cx, |_, editor, cx| {
for _ in 0..times.unwrap_or(1) {
editor.undo(&editor::actions::Undo, cx);
}
});
})
});
workspace.register_action(|_: &mut Workspace, _: &Redo, cx| {
Vim::update(cx, |vim, cx| {
let times = vim.take_count(cx);
vim.update_active_editor(cx, |_, editor, cx| {
for _ in 0..times.unwrap_or(1) {
editor.redo(&editor::actions::Redo, cx);
}
});
})
});
paste::register(workspace, cx);
repeat::register(workspace, cx);
scroll::register(workspace, cx);
search::register(workspace, cx);
substitute::register(workspace, cx);
increment::register(workspace, cx);
}
pub fn normal_motion(
motion: Motion,
operator: Option<Operator>,
times: Option<usize>,
cx: &mut WindowContext,
) {
Vim::update(cx, |vim, cx| {
match operator {
None => move_cursor(vim, motion, times, cx),
Some(Operator::Change) => change_motion(vim, motion, times, cx),
Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
Some(Operator::AddSurrounds { target: None }) => {}
Some(Operator::Indent) => indent_motion(vim, motion, times, IndentDirection::In, cx),
Some(Operator::Outdent) => indent_motion(vim, motion, times, IndentDirection::Out, cx),
Some(Operator::Lowercase) => {
change_case_motion(vim, motion, times, CaseTarget::Lowercase, cx)
}
Some(Operator::Uppercase) => {
change_case_motion(vim, motion, times, CaseTarget::Uppercase, cx)
}
Some(Operator::OppositeCase) => {
change_case_motion(vim, motion, times, CaseTarget::OppositeCase, cx)
}
Some(Operator::ToggleComments) => toggle_comments_motion(vim, motion, times, cx),
Some(operator) => {
// Can't do anything for text objects, Ignoring
error!("Unexpected normal mode motion operator: {:?}", operator)
}
}
});
}
pub fn normal_object(object: Object, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
let mut waiting_operator: Option<Operator> = None;
match vim.maybe_pop_operator() {
Some(Operator::Object { around }) => match vim.maybe_pop_operator() {
Some(Operator::Change) => change_object(vim, object, around, cx),
Some(Operator::Delete) => delete_object(vim, object, around, cx),
Some(Operator::Yank) => yank_object(vim, object, around, cx),
Some(Operator::Indent) => {
indent_object(vim, object, around, IndentDirection::In, cx)
}
Some(Operator::Outdent) => {
indent_object(vim, object, around, IndentDirection::Out, cx)
}
Some(Operator::Lowercase) => {
change_case_object(vim, object, around, CaseTarget::Lowercase, cx)
}
Some(Operator::Uppercase) => {
change_case_object(vim, object, around, CaseTarget::Uppercase, cx)
}
Some(Operator::OppositeCase) => {
change_case_object(vim, object, around, CaseTarget::OppositeCase, cx)
}
Some(Operator::AddSurrounds { target: None }) => {
waiting_operator = Some(Operator::AddSurrounds {
target: Some(SurroundsType::Object(object)),
});
}
Some(Operator::ToggleComments) => toggle_comments_object(vim, object, around, cx),
_ => {
// Can't do anything for namespace operators. Ignoring
}
},
Some(Operator::DeleteSurrounds) => {
waiting_operator = Some(Operator::DeleteSurrounds);
}
Some(Operator::ChangeSurrounds { target: None }) => {
if check_and_move_to_valid_bracket_pair(vim, object, cx) {
waiting_operator = Some(Operator::ChangeSurrounds {
target: Some(object),
});
}
}
_ => {
// Can't do anything with change/delete/yank/surrounds and text objects. Ignoring
}
}
vim.clear_operator(cx);
if let Some(operator) = waiting_operator {
vim.push_operator(operator, cx);
}
});
}
pub(crate) fn move_cursor(
vim: &mut Vim,
motion: Motion,
times: Option<usize>,
cx: &mut WindowContext,
) {
vim.update_active_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
motion
.move_point(map, cursor, goal, times, &text_layout_details)
.unwrap_or((cursor, goal))
})
})
});
}
fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
});
});
});
}
fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
});
}
fn insert_first_non_whitespace(
_: &mut Workspace,
_: &InsertFirstNonWhitespace,
cx: &mut ViewContext<Workspace>,
) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(
first_non_whitespace(map, false, cursor),
SelectionGoal::None,
)
});
});
});
});
}
fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(next_line_end(map, cursor, 1), SelectionGoal::None)
});
});
});
});
}
fn insert_at_previous(_: &mut Workspace, _: &InsertAtPrevious, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |vim, editor, cx| {
if let Some(marks) = vim.state().marks.get("^") {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
});
}
});
});
}
fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let selections = editor.selections.all::<Point>(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let selection_start_rows: BTreeSet<u32> = selections
.into_iter()
.map(|selection| selection.start.row)
.collect();
let edits = selection_start_rows.into_iter().map(|row| {
let indent = snapshot
.indent_size_for_line(MultiBufferRow(row))
.chars()
.collect::<String>();
let start_of_line = Point::new(row, 0);
(start_of_line..start_of_line, indent + "\n")
});
editor.edit_with_autoindent(edits, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| {
let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
let insert_point = motion::end_of_line(map, false, previous_line, 1);
(insert_point, SelectionGoal::None)
});
});
});
});
});
}
fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
let selections = editor.selections.all::<Point>(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let selection_end_rows: BTreeSet<u32> = selections
.into_iter()
.map(|selection| selection.end.row)
.collect();
let edits = selection_end_rows.into_iter().map(|row| {
let indent = snapshot
.indent_size_for_line(MultiBufferRow(row))
.chars()
.collect::<String>();
let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
(end_of_line..end_of_line, "\n".to_string() + &indent)
});
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::CurrentLine.move_point(
map,
cursor,
goal,
None,
&text_layout_details,
)
});
});
editor.edit_with_autoindent(edits, cx);
});
});
});
}
fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
let count = vim.take_count(cx);
yank_motion(vim, motion::Motion::CurrentLine, count, cx)
})
}
fn yank_to_end_of_line(_: &mut Workspace, _: &YankToEndOfLine, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let count = vim.take_count(cx);
yank_motion(
vim,
motion::Motion::EndOfLine {
display_lines: false,
},
count,
cx,
)
})
}
fn toggle_comments(_: &mut Workspace, _: &ToggleComments, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let mut original_positions = save_selection_starts(editor, cx);
editor.toggle_comments(&Default::default(), cx);
restore_selection_cursors(editor, cx, &mut original_positions);
});
});
if vim.state().mode.is_visual() {
vim.switch_mode(Mode::Normal, false, cx)
}
});
}
fn save_selection_starts(editor: &Editor, cx: &mut ViewContext<Editor>) -> HashMap<usize, Anchor> {
let (map, selections) = editor.selections.all_display(cx);
selections
.iter()
.map(|selection| {
(
selection.id,
map.display_point_to_anchor(selection.start, Bias::Right),
)
})
.collect::<HashMap<_, _>>()
}
fn restore_selection_cursors(
editor: &mut Editor,
cx: &mut ViewContext<Editor>,
positions: &mut HashMap<usize, Anchor>,
) {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
if let Some(anchor) = positions.remove(&selection.id) {
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
}
});
});
}
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
let count = vim.take_count(cx).unwrap_or(1);
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let (map, display_selections) = editor.selections.all_display(cx);
let mut edits = Vec::new();
for selection in display_selections {
let mut range = selection.range();
for _ in 0..count {
let new_point = movement::saturating_right(&map, range.end);
if range.end == new_point {
return;
}
range.end = new_point;
}
edits.push((
range.start.to_offset(&map, Bias::Left)
..range.end.to_offset(&map, Bias::Left),
text.repeat(count),
))
}
editor.buffer().update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
let point = movement::saturating_left(map, selection.head());
selection.collapse_to(point, SelectionGoal::None)
});
});
});
});
vim.pop_operator(cx)
});
}
#[cfg(test)]
mod test {
use gpui::{KeyBinding, TestAppContext};
use indoc::indoc;
use settings::SettingsStore;
use crate::{
motion,
state::Mode::{self},
test::{NeovimBackedTestContext, VimTestContext},
VimSettings,
};
#[gpui::test]
async fn test_h(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset(
"h",
indoc! {"
ˇThe qˇuick
ˇbrown"
},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_backspace(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset(
"backspace",
indoc! {"
ˇThe qˇuick
ˇbrown"
},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_j(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {"
aaˇaa
😃😃"
})
.await;
cx.simulate_shared_keystrokes("j").await;
cx.shared_state().await.assert_eq(indoc! {"
aaaa
😃ˇ😃"
});
cx.simulate_at_each_offset(
"j",
indoc! {"
ˇThe qˇuick broˇwn
ˇfox jumps"
},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_enter(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset(
"enter",
indoc! {"
ˇThe qˇuick broˇwn
ˇfox jumps"
},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_k(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset(
"k",
indoc! {"
ˇThe qˇuick
ˇbrown fˇox jumˇps"
},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_l(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset(
"l",
indoc! {"
ˇThe qˇuicˇk
ˇbrowˇn"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset(
"$",
indoc! {"
ˇThe qˇuicˇk
ˇbrowˇn"},
)
.await
.assert_matches();
cx.simulate_at_each_offset(
"0",
indoc! {"
ˇThe qˇuicˇk
ˇbrowˇn"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset(
"shift-g",
indoc! {"
The ˇquick
brown fox jumps
overˇ the lazy doˇg"},
)
.await
.assert_matches();
cx.simulate(
"shift-g",
indoc! {"
The quiˇck
brown"},
)
.await
.assert_matches();
cx.simulate(
"shift-g",
indoc! {"
The quiˇck
"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_w(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset(
"w",
indoc! {"
The ˇquickˇ-ˇbrown
ˇ
ˇ
ˇfox_jumps ˇover
ˇthˇe"},
)
.await
.assert_matches();
cx.simulate_at_each_offset(
"shift-w",
indoc! {"
The ˇquickˇ-ˇbrown
ˇ
ˇ
ˇfox_jumps ˇover
ˇthˇe"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset(
"e",
indoc! {"
Thˇe quicˇkˇ-browˇn
fox_jumpˇs oveˇr
thˇe"},
)
.await
.assert_matches();
cx.simulate_at_each_offset(
"shift-e",
indoc! {"
Thˇe quicˇkˇ-browˇn
fox_jumpˇs oveˇr
thˇe"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_b(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset(
"b",
indoc! {"
ˇThe ˇquickˇ-ˇbrown
ˇ
ˇ
ˇfox_jumps ˇover
ˇthe"},
)
.await
.assert_matches();
cx.simulate_at_each_offset(
"shift-b",
indoc! {"
ˇThe ˇquickˇ-ˇbrown
ˇ
ˇ
ˇfox_jumps ˇover
ˇthe"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_gg(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset(
"g g",
indoc! {"
The qˇuick
brown fox jumps
over ˇthe laˇzy dog"},
)
.await
.assert_matches();
cx.simulate(
"g g",
indoc! {"
brown fox jumps
over the laˇzy dog"},
)
.await
.assert_matches();
cx.simulate(
"2 g g",
indoc! {"
ˇ
brown fox jumps
over the lazydog"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset(
"shift-g",
indoc! {"
The qˇuick
brown fox jumps
over ˇthe laˇzy dog"},
)
.await
.assert_matches();
cx.simulate(
"shift-g",
indoc! {"
brown fox jumps
over the laˇzy dog"},
)
.await
.assert_matches();
cx.simulate(
"2 shift-g",
indoc! {"
ˇ
brown fox jumps
over the lazydog"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_a(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset("a", "The qˇuicˇk")
.await
.assert_matches();
}
#[gpui::test]
async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset(
"shift-a",
indoc! {"
ˇ
The qˇuick
brown ˇfox "},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate("^", "The qˇuick").await.assert_matches();
cx.simulate("^", " The qˇuick").await.assert_matches();
cx.simulate("^", "ˇ").await.assert_matches();
cx.simulate(
"^",
indoc! {"
The qˇuick
brown fox"},
)
.await
.assert_matches();
cx.simulate(
"^",
indoc! {"
ˇ
The quick"},
)
.await
.assert_matches();
// Indoc disallows trailing whitespace.
cx.simulate("^", " ˇ \nThe quick").await.assert_matches();
}
#[gpui::test]
async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate("shift-i", "The qˇuick").await.assert_matches();
cx.simulate("shift-i", " The qˇuick").await.assert_matches();
cx.simulate("shift-i", "ˇ").await.assert_matches();
cx.simulate(
"shift-i",
indoc! {"
The qˇuick
brown fox"},
)
.await
.assert_matches();
cx.simulate(
"shift-i",
indoc! {"
ˇ
The quick"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate(
"shift-d",
indoc! {"
The qˇuick
brown fox"},
)
.await
.assert_matches();
cx.simulate(
"shift-d",
indoc! {"
The quick
ˇ
brown fox"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_x(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
.await
.assert_matches();
cx.simulate(
"x",
indoc! {"
Tesˇt
test"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_delete_left(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
.await
.assert_matches();
cx.simulate(
"shift-x",
indoc! {"
Test
ˇtest"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_o(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate("o", "ˇ").await.assert_matches();
cx.simulate("o", "The ˇquick").await.assert_matches();
cx.simulate_at_each_offset(
"o",
indoc! {"
The qˇuick
brown ˇfox
jumps ˇover"},
)
.await
.assert_matches();
cx.simulate(
"o",
indoc! {"
The quick
ˇ
brown fox"},
)
.await
.assert_matches();
cx.assert_binding(
"o",
indoc! {"
fn test() {
println!(ˇ);
}"},
Mode::Normal,
indoc! {"
fn test() {
println!();
ˇ
}"},
Mode::Insert,
);
cx.assert_binding(
"o",
indoc! {"
fn test(ˇ) {
println!();
}"},
Mode::Normal,
indoc! {"
fn test() {
ˇ
println!();
}"},
Mode::Insert,
);
}
#[gpui::test]
async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate("shift-o", "ˇ").await.assert_matches();
cx.simulate("shift-o", "The ˇquick").await.assert_matches();
cx.simulate_at_each_offset(
"shift-o",
indoc! {"
The qˇuick
brown ˇfox
jumps ˇover"},
)
.await
.assert_matches();
cx.simulate(
"shift-o",
indoc! {"
The quick
ˇ
brown fox"},
)
.await
.assert_matches();
// Our indentation is smarter than vims. So we don't match here
cx.assert_binding(
"shift-o",
indoc! {"
fn test() {
println!(ˇ);
}"},
Mode::Normal,
indoc! {"
fn test() {
ˇ
println!();
}"},
Mode::Insert,
);
cx.assert_binding(
"shift-o",
indoc! {"
fn test(ˇ) {
println!();
}"},
Mode::Normal,
indoc! {"
ˇ
fn test() {
println!();
}"},
Mode::Insert,
);
}
#[gpui::test]
async fn test_dd(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate("d d", "ˇ").await.assert_matches();
cx.simulate("d d", "The ˇquick").await.assert_matches();
cx.simulate_at_each_offset(
"d d",
indoc! {"
The qˇuick
brown ˇfox
jumps ˇover"},
)
.await
.assert_matches();
cx.simulate(
"d d",
indoc! {"
The quick
ˇ
brown fox"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_cc(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate("c c", "ˇ").await.assert_matches();
cx.simulate("c c", "The ˇquick").await.assert_matches();
cx.simulate_at_each_offset(
"c c",
indoc! {"
The quˇick
brown ˇfox
jumps ˇover"},
)
.await
.assert_matches();
cx.simulate(
"c c",
indoc! {"
The quick
ˇ
brown fox"},
)
.await
.assert_matches();
}
#[gpui::test]
async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
for count in 1..=5 {
cx.simulate_at_each_offset(
&format!("{count} w"),
indoc! {"
ˇThe quˇickˇ browˇn
ˇ
ˇfox ˇjumpsˇ-ˇoˇver
ˇthe lazy dog
"},
)
.await
.assert_matches();
}
}
#[gpui::test]
async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
.await
.assert_matches();
}
#[gpui::test]
async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
for count in 1..=3 {
let test_case = indoc! {"
ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
ˇ ˇbˇaaˇa ˇbˇbˇb
ˇ
ˇb
"};
cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
.await
.assert_matches();
cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
.await
.assert_matches();
}
}
#[gpui::test]
async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
let test_case = indoc! {"
ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
ˇ ˇbˇaaˇa ˇbˇbˇb
ˇ•••
ˇb
"
};
for count in 1..=3 {
cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
.await
.assert_matches();
cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
.await
.assert_matches();
}
}
#[gpui::test]
async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<VimSettings>(cx, |s| {
s.use_multiline_find = Some(true);
});
});
cx.assert_binding(
"f l",
indoc! {"
ˇfunction print() {
console.log('ok')
}
"},
Mode::Normal,
indoc! {"
function print() {
consoˇle.log('ok')
}
"},
Mode::Normal,
);
cx.assert_binding(
"t l",
indoc! {"
ˇfunction print() {
console.log('ok')
}
"},
Mode::Normal,
indoc! {"
function print() {
consˇole.log('ok')
}
"},
Mode::Normal,
);
}
#[gpui::test]
async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<VimSettings>(cx, |s| {
s.use_multiline_find = Some(true);
});
});
cx.assert_binding(
"shift-f p",
indoc! {"
function print() {
console.ˇlog('ok')
}
"},
Mode::Normal,
indoc! {"
function ˇprint() {
console.log('ok')
}
"},
Mode::Normal,
);
cx.assert_binding(
"shift-t p",
indoc! {"
function print() {
console.ˇlog('ok')
}
"},
Mode::Normal,
indoc! {"
function pˇrint() {
console.log('ok')
}
"},
Mode::Normal,
);
}
#[gpui::test]
async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<VimSettings>(cx, |s| {
s.use_smartcase_find = Some(true);
});
});
cx.assert_binding(
"f p",
indoc! {"ˇfmt.Println(\"Hello, World!\")"},
Mode::Normal,
indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
Mode::Normal,
);
cx.assert_binding(
"shift-f p",
indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
Mode::Normal,
indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
Mode::Normal,
);
cx.assert_binding(
"t p",
indoc! {"ˇfmt.Println(\"Hello, World!\")"},
Mode::Normal,
indoc! {"fmtˇ.Println(\"Hello, World!\")"},
Mode::Normal,
);
cx.assert_binding(
"shift-t p",
indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
Mode::Normal,
indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
Mode::Normal,
);
}
#[gpui::test]
async fn test_percent(cx: &mut TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
.await
.assert_matches();
cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
.await
.assert_matches();
cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
.await
.assert_matches();
}
#[gpui::test]
async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
// goes to current line end
cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
cx.simulate_shared_keystrokes("$").await;
cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
// goes to next line end
cx.simulate_shared_keystrokes("2 $").await;
cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
// try to exceed the final line.
cx.simulate_shared_keystrokes("4 $").await;
cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
}
#[gpui::test]
async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.update(|cx| {
cx.bind_keys(vec![
KeyBinding::new(
"w",
motion::NextSubwordStart {
ignore_punctuation: false,
},
Some("Editor && VimControl && !VimWaiting && !menu"),
),
KeyBinding::new(
"b",
motion::PreviousSubwordStart {
ignore_punctuation: false,
},
Some("Editor && VimControl && !VimWaiting && !menu"),
),
KeyBinding::new(
"e",
motion::NextSubwordEnd {
ignore_punctuation: false,
},
Some("Editor && VimControl && !VimWaiting && !menu"),
),
KeyBinding::new(
"g e",
motion::PreviousSubwordEnd {
ignore_punctuation: false,
},
Some("Editor && VimControl && !VimWaiting && !menu"),
),
]);
});
cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
// Special case: In 'cw', 'w' acts like 'e'
cx.assert_binding(
"c w",
indoc! {"ˇassert_binding"},
Mode::Normal,
indoc! {"ˇ_binding"},
Mode::Insert,
);
cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
cx.assert_binding_normal(
"g e",
indoc! {"assert_bindinˇg"},
indoc! {"asserˇt_binding"},
);
}
#[gpui::test]
async fn test_shift_y(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state("helˇlo\n").await;
cx.simulate_shared_keystrokes("shift-y").await;
cx.shared_clipboard().await.assert_eq("lo");
}
#[gpui::test]
async fn test_r(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state("ˇhello\n").await;
cx.simulate_shared_keystrokes("r -").await;
cx.shared_state().await.assert_eq("ˇ-ello\n");
cx.set_shared_state("ˇhello\n").await;
cx.simulate_shared_keystrokes("3 r -").await;
cx.shared_state().await.assert_eq("--ˇ-lo\n");
cx.set_shared_state("ˇhello\n").await;
cx.simulate_shared_keystrokes("r - 2 l .").await;
cx.shared_state().await.assert_eq("-eˇ-lo\n");
cx.set_shared_state("ˇhello world\n").await;
cx.simulate_shared_keystrokes("2 r - f w .").await;
cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
cx.set_shared_state("ˇhello world\n").await;
cx.simulate_shared_keystrokes("2 0 r - ").await;
cx.shared_state().await.assert_eq("ˇhello world\n");
}
}