mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-28 15:13:09 +03:00
Add c and d operators to vim normal mode
Extracted motions from normal mode Changed vim_submode to be vim_operator to enable better composition of operators
This commit is contained in:
parent
670757e5c9
commit
63278041e1
@ -1,58 +1,93 @@
|
|||||||
{
|
{
|
||||||
"Editor && vim_mode == insert": {
|
"Editor && VimControl": {
|
||||||
"escape": "vim::NormalBefore",
|
|
||||||
"ctrl-c": "vim::NormalBefore"
|
|
||||||
},
|
|
||||||
"Editor && vim_mode == normal && vim_submode == g": {
|
|
||||||
"g": "vim::MoveToStart",
|
|
||||||
"escape": [
|
|
||||||
"vim::SwitchMode",
|
|
||||||
{
|
|
||||||
"Normal": "None"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Editor && vim_mode == normal": {
|
|
||||||
"i": [
|
"i": [
|
||||||
"vim::SwitchMode",
|
"vim::SwitchMode",
|
||||||
"Insert"
|
"Insert"
|
||||||
],
|
],
|
||||||
"g": [
|
"g": [
|
||||||
"vim::SwitchMode",
|
"vim::PushOperator",
|
||||||
{
|
{
|
||||||
"Normal": "GPrefix"
|
"Namespace": "G"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"h": "vim::MoveLeft",
|
"h": "vim::Left",
|
||||||
"j": "vim::MoveDown",
|
"j": "vim::Down",
|
||||||
"k": "vim::MoveUp",
|
"k": "vim::Up",
|
||||||
"l": "vim::MoveRight",
|
"l": "vim::Right",
|
||||||
"0": "vim::MoveToStartOfLine",
|
"0": "vim::StartOfLine",
|
||||||
"shift-$": "vim::MoveToEndOfLine",
|
"shift-$": "vim::EndOfLine",
|
||||||
"shift-G": "vim::MoveToEnd",
|
"shift-G": "vim::EndOfDocument",
|
||||||
|
"w": "vim::NextWordStart",
|
||||||
|
"shift-W": [
|
||||||
|
"vim::NextWordStart",
|
||||||
|
{
|
||||||
|
"ignorePunctuation": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"e": "vim::NextWordEnd",
|
||||||
|
"shift-E": [
|
||||||
|
"vim::NextWordEnd",
|
||||||
|
{
|
||||||
|
"ignorePunctuation": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"b": "vim::PreviousWordStart",
|
||||||
|
"shift-B": [
|
||||||
|
"vim::PreviousWordStart",
|
||||||
|
{
|
||||||
|
"ignorePunctuation": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"escape": [
|
||||||
|
"vim::SwitchMode",
|
||||||
|
"Normal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Editor && vim_operator == g": {
|
||||||
|
"g": "vim::StartOfDocument"
|
||||||
|
},
|
||||||
|
"Editor && vim_mode == insert": {
|
||||||
|
"escape": "vim::NormalBefore",
|
||||||
|
"ctrl-c": "vim::NormalBefore"
|
||||||
|
},
|
||||||
|
"Editor && vim_mode == normal": {
|
||||||
|
"c": [
|
||||||
|
"vim::PushOperator",
|
||||||
|
"Change"
|
||||||
|
],
|
||||||
|
"d": [
|
||||||
|
"vim::PushOperator",
|
||||||
|
"Delete"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Editor && vim_operator == c": {
|
||||||
"w": [
|
"w": [
|
||||||
"vim::MoveToNextWordStart",
|
"vim::NextWordEnd",
|
||||||
false
|
{
|
||||||
|
"ignorePunctuation": false
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"shift-W": [
|
"shift-W": [
|
||||||
"vim::MoveToNextWordStart",
|
"vim::NextWordEnd",
|
||||||
true
|
{
|
||||||
|
"ignorePunctuation": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Editor && vim_operator == d": {
|
||||||
|
"w": [
|
||||||
|
"vim::NextWordStart",
|
||||||
|
{
|
||||||
|
"ignorePunctuation": false,
|
||||||
|
"stopAtNewline": true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"e": [
|
"shift-W": [
|
||||||
"vim::MoveToNextWordEnd",
|
"vim::NextWordStart",
|
||||||
false
|
{
|
||||||
],
|
"ignorePunctuation": true,
|
||||||
"shift-E": [
|
"stopAtNewline": true
|
||||||
"vim::MoveToNextWordEnd",
|
}
|
||||||
true
|
|
||||||
],
|
|
||||||
"b": [
|
|
||||||
"vim::MoveToPreviousWordStart",
|
|
||||||
false
|
|
||||||
],
|
|
||||||
"shift-B": [
|
|
||||||
"vim::MoveToPreviousWordStart",
|
|
||||||
true
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
use editor::{EditorBlurred, EditorCreated, EditorFocused, EditorMode, EditorReleased};
|
use editor::{EditorBlurred, EditorCreated, EditorFocused, EditorMode, EditorReleased};
|
||||||
use gpui::MutableAppContext;
|
use gpui::MutableAppContext;
|
||||||
|
|
||||||
use crate::{mode::Mode, SwitchMode, VimState};
|
use crate::{state::Mode, Vim};
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.subscribe_global(editor_created).detach();
|
cx.subscribe_global(editor_created).detach();
|
||||||
@ -11,9 +11,9 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) {
|
fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) {
|
||||||
cx.update_default_global(|vim_state: &mut VimState, cx| {
|
cx.update_default_global(|vim: &mut Vim, cx| {
|
||||||
vim_state.editors.insert(editor.id(), editor.downgrade());
|
vim.editors.insert(editor.id(), editor.downgrade());
|
||||||
vim_state.sync_editor_options(cx);
|
vim.sync_editor_options(cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,17 +21,17 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont
|
|||||||
let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) {
|
let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) {
|
||||||
Mode::Insert
|
Mode::Insert
|
||||||
} else {
|
} else {
|
||||||
Mode::normal()
|
Mode::Normal
|
||||||
};
|
};
|
||||||
|
|
||||||
VimState::update_global(cx, |state, cx| {
|
Vim::update(cx, |state, cx| {
|
||||||
state.active_editor = Some(editor.downgrade());
|
state.active_editor = Some(editor.downgrade());
|
||||||
state.switch_mode(&SwitchMode(mode), cx);
|
state.switch_mode(mode, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) {
|
fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) {
|
||||||
VimState::update_global(cx, |state, cx| {
|
Vim::update(cx, |state, cx| {
|
||||||
if let Some(previous_editor) = state.active_editor.clone() {
|
if let Some(previous_editor) = state.active_editor.clone() {
|
||||||
if previous_editor == editor.clone() {
|
if previous_editor == editor.clone() {
|
||||||
state.active_editor = None;
|
state.active_editor = None;
|
||||||
@ -42,11 +42,11 @@ fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) {
|
fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) {
|
||||||
cx.update_default_global(|vim_state: &mut VimState, _| {
|
cx.update_default_global(|vim: &mut Vim, _| {
|
||||||
vim_state.editors.remove(&editor.id());
|
vim.editors.remove(&editor.id());
|
||||||
if let Some(previous_editor) = vim_state.active_editor.clone() {
|
if let Some(previous_editor) = vim.active_editor.clone() {
|
||||||
if previous_editor == editor.clone() {
|
if previous_editor == editor.clone() {
|
||||||
vim_state.active_editor = None;
|
vim.active_editor = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::{mode::Mode, SwitchMode, VimState};
|
use crate::{state::Mode, Vim};
|
||||||
use editor::Bias;
|
use editor::Bias;
|
||||||
use gpui::{actions, MutableAppContext, ViewContext};
|
use gpui::{actions, MutableAppContext, ViewContext};
|
||||||
use language::SelectionGoal;
|
use language::SelectionGoal;
|
||||||
@ -11,30 +11,30 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||||
VimState::update_global(cx, |state, cx| {
|
Vim::update(cx, |state, cx| {
|
||||||
state.update_active_editor(cx, |editor, cx| {
|
state.update_active_editor(cx, |editor, cx| {
|
||||||
editor.move_cursors(cx, |map, mut cursor, _| {
|
editor.move_cursors(cx, |map, mut cursor, _| {
|
||||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
state.switch_mode(&SwitchMode(Mode::normal()), cx);
|
state.switch_mode(Mode::Normal, cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::{mode::Mode, vim_test_context::VimTestContext};
|
use crate::{state::Mode, vim_test_context::VimTestContext};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = VimTestContext::new(cx, true, "").await;
|
let mut cx = VimTestContext::new(cx, true, "").await;
|
||||||
cx.simulate_keystroke("i");
|
cx.simulate_keystroke("i");
|
||||||
assert_eq!(cx.mode(), Mode::Insert);
|
assert_eq!(cx.mode(), Mode::Insert);
|
||||||
cx.simulate_keystrokes(&["T", "e", "s", "t"]);
|
cx.simulate_keystrokes(["T", "e", "s", "t"]);
|
||||||
cx.assert_editor_state("Test|");
|
cx.assert_editor_state("Test|");
|
||||||
cx.simulate_keystroke("escape");
|
cx.simulate_keystroke("escape");
|
||||||
assert_eq!(cx.mode(), Mode::normal());
|
assert_eq!(cx.mode(), Mode::Normal);
|
||||||
cx.assert_editor_state("Tes|t");
|
cx.assert_editor_state("Tes|t");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,73 +0,0 @@
|
|||||||
use editor::CursorShape;
|
|
||||||
use gpui::keymap::Context;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
|
||||||
pub enum Mode {
|
|
||||||
Normal(NormalState),
|
|
||||||
Insert,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mode {
|
|
||||||
pub fn cursor_shape(&self) -> CursorShape {
|
|
||||||
match self {
|
|
||||||
Mode::Normal(_) => CursorShape::Block,
|
|
||||||
Mode::Insert => CursorShape::Bar,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn keymap_context_layer(&self) -> Context {
|
|
||||||
let mut context = Context::default();
|
|
||||||
context.map.insert(
|
|
||||||
"vim_mode".to_string(),
|
|
||||||
match self {
|
|
||||||
Self::Normal(_) => "normal",
|
|
||||||
Self::Insert => "insert",
|
|
||||||
}
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Self::Normal(normal_state) => normal_state.set_context(&mut context),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
context
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn normal() -> Mode {
|
|
||||||
Mode::Normal(Default::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Mode {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Normal(Default::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
|
||||||
pub enum NormalState {
|
|
||||||
None,
|
|
||||||
GPrefix,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NormalState {
|
|
||||||
pub fn set_context(&self, context: &mut Context) {
|
|
||||||
let submode = match self {
|
|
||||||
Self::GPrefix => Some("g"),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(submode) = submode {
|
|
||||||
context
|
|
||||||
.map
|
|
||||||
.insert("vim_submode".to_string(), submode.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for NormalState {
|
|
||||||
fn default() -> Self {
|
|
||||||
NormalState::None
|
|
||||||
}
|
|
||||||
}
|
|
296
crates/vim/src/motion.rs
Normal file
296
crates/vim/src/motion.rs
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
use editor::{
|
||||||
|
char_kind,
|
||||||
|
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||||
|
movement, Bias, DisplayPoint,
|
||||||
|
};
|
||||||
|
use gpui::{actions, impl_actions, MutableAppContext};
|
||||||
|
use language::{Selection, SelectionGoal};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
normal::normal_motion,
|
||||||
|
state::{Mode, Operator},
|
||||||
|
Vim,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum Motion {
|
||||||
|
Left,
|
||||||
|
Down,
|
||||||
|
Up,
|
||||||
|
Right,
|
||||||
|
NextWordStart {
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
stop_at_newline: bool,
|
||||||
|
},
|
||||||
|
NextWordEnd {
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
},
|
||||||
|
PreviousWordStart {
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
},
|
||||||
|
StartOfLine,
|
||||||
|
EndOfLine,
|
||||||
|
StartOfDocument,
|
||||||
|
EndOfDocument,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct NextWordStart {
|
||||||
|
#[serde(default)]
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
stop_at_newline: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct NextWordEnd {
|
||||||
|
#[serde(default)]
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct PreviousWordStart {
|
||||||
|
#[serde(default)]
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
vim,
|
||||||
|
[
|
||||||
|
Left,
|
||||||
|
Down,
|
||||||
|
Up,
|
||||||
|
Right,
|
||||||
|
StartOfLine,
|
||||||
|
EndOfLine,
|
||||||
|
StartOfDocument,
|
||||||
|
EndOfDocument
|
||||||
|
]
|
||||||
|
);
|
||||||
|
impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
|
||||||
|
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, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
|
||||||
|
motion(Motion::StartOfDocument, cx)
|
||||||
|
});
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
|
||||||
|
|
||||||
|
cx.add_action(
|
||||||
|
|_: &mut Workspace,
|
||||||
|
&NextWordStart {
|
||||||
|
ignore_punctuation,
|
||||||
|
stop_at_newline,
|
||||||
|
}: &NextWordStart,
|
||||||
|
cx: _| {
|
||||||
|
motion(
|
||||||
|
Motion::NextWordStart {
|
||||||
|
ignore_punctuation,
|
||||||
|
stop_at_newline,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
cx.add_action(
|
||||||
|
|_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
|
||||||
|
motion(Motion::NextWordEnd { ignore_punctuation }, cx)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
cx.add_action(
|
||||||
|
|_: &mut Workspace,
|
||||||
|
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
|
||||||
|
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||||
|
Vim::update(cx, |vim, cx| {
|
||||||
|
if let Some(Operator::Namespace(_)) = vim.active_operator() {
|
||||||
|
vim.pop_operator(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
match Vim::read(cx).state.mode {
|
||||||
|
Mode::Normal => normal_motion(motion, cx),
|
||||||
|
Mode::Insert => panic!("motion bindings in insert mode interfere with normal typing"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Motion {
|
||||||
|
pub fn move_point(
|
||||||
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
point: DisplayPoint,
|
||||||
|
goal: SelectionGoal,
|
||||||
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
|
use Motion::*;
|
||||||
|
match self {
|
||||||
|
Left => (left(map, point), SelectionGoal::None),
|
||||||
|
Down => movement::down(map, point, goal),
|
||||||
|
Up => movement::up(map, point, goal),
|
||||||
|
Right => (right(map, point), SelectionGoal::None),
|
||||||
|
NextWordStart {
|
||||||
|
ignore_punctuation,
|
||||||
|
stop_at_newline,
|
||||||
|
} => (
|
||||||
|
next_word_start(map, point, ignore_punctuation, stop_at_newline),
|
||||||
|
SelectionGoal::None,
|
||||||
|
),
|
||||||
|
NextWordEnd { ignore_punctuation } => (
|
||||||
|
next_word_end(map, point, ignore_punctuation, true),
|
||||||
|
SelectionGoal::None,
|
||||||
|
),
|
||||||
|
PreviousWordStart { ignore_punctuation } => (
|
||||||
|
previous_word_start(map, point, ignore_punctuation),
|
||||||
|
SelectionGoal::None,
|
||||||
|
),
|
||||||
|
StartOfLine => (
|
||||||
|
movement::line_beginning(map, point, false),
|
||||||
|
SelectionGoal::None,
|
||||||
|
),
|
||||||
|
EndOfLine => (
|
||||||
|
map.clip_point(movement::line_end(map, point, false), Bias::Left),
|
||||||
|
SelectionGoal::None,
|
||||||
|
),
|
||||||
|
StartOfDocument => (start_of_document(map), SelectionGoal::None),
|
||||||
|
EndOfDocument => (end_of_document(map), SelectionGoal::None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_selection(self, map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {
|
||||||
|
use Motion::*;
|
||||||
|
match self {
|
||||||
|
Up => {
|
||||||
|
let (start, _) = Up.move_point(map, selection.start, SelectionGoal::None);
|
||||||
|
// Cursor at top of file. Return early rather
|
||||||
|
if start == selection.start {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (start, _) = StartOfLine.move_point(map, start, SelectionGoal::None);
|
||||||
|
let (end, _) = EndOfLine.move_point(map, selection.end, SelectionGoal::None);
|
||||||
|
selection.start = start;
|
||||||
|
selection.end = end;
|
||||||
|
// TODO: Make sure selection goal is correct here
|
||||||
|
selection.goal = SelectionGoal::None;
|
||||||
|
}
|
||||||
|
Down => {
|
||||||
|
let (end, _) = Down.move_point(map, selection.end, SelectionGoal::None);
|
||||||
|
// Cursor at top of file. Return early rather
|
||||||
|
if end == selection.start {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (start, _) = StartOfLine.move_point(map, selection.start, SelectionGoal::None);
|
||||||
|
let (end, _) = EndOfLine.move_point(map, end, SelectionGoal::None);
|
||||||
|
selection.start = start;
|
||||||
|
selection.end = end;
|
||||||
|
// TODO: Make sure selection goal is correct here
|
||||||
|
selection.goal = SelectionGoal::None;
|
||||||
|
}
|
||||||
|
NextWordEnd { ignore_punctuation } => {
|
||||||
|
selection.set_head(
|
||||||
|
next_word_end(map, selection.head(), ignore_punctuation, false),
|
||||||
|
SelectionGoal::None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let (head, goal) = self.move_point(map, selection.head(), selection.goal);
|
||||||
|
selection.set_head(head, goal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||||
|
*point.column_mut() = point.column().saturating_sub(1);
|
||||||
|
map.clip_point(point, Bias::Left)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||||
|
*point.column_mut() += 1;
|
||||||
|
map.clip_point(point, Bias::Right)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_word_start(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
point: DisplayPoint,
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
stop_at_newline: bool,
|
||||||
|
) -> DisplayPoint {
|
||||||
|
let mut crossed_newline = false;
|
||||||
|
movement::find_boundary(map, point, |left, right| {
|
||||||
|
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||||
|
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||||
|
let at_newline = right == '\n';
|
||||||
|
|
||||||
|
let found = (left_kind != right_kind && !right.is_whitespace())
|
||||||
|
|| (at_newline && (crossed_newline || stop_at_newline))
|
||||||
|
|| (at_newline && left == '\n'); // Prevents skipping repeated empty lines
|
||||||
|
|
||||||
|
if at_newline {
|
||||||
|
crossed_newline = true;
|
||||||
|
}
|
||||||
|
found
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_word_end(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
mut point: DisplayPoint,
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
before_end_character: bool,
|
||||||
|
) -> DisplayPoint {
|
||||||
|
*point.column_mut() += 1;
|
||||||
|
point = movement::find_boundary(map, point, |left, right| {
|
||||||
|
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||||
|
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||||
|
|
||||||
|
left_kind != right_kind && !left.is_whitespace()
|
||||||
|
});
|
||||||
|
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
|
||||||
|
// we have backtraced already
|
||||||
|
if before_end_character
|
||||||
|
&& !map
|
||||||
|
.chars_at(point)
|
||||||
|
.skip(1)
|
||||||
|
.next()
|
||||||
|
.map(|c| c == '\n')
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
*point.column_mut() = point.column().saturating_sub(1);
|
||||||
|
}
|
||||||
|
map.clip_point(point, Bias::Left)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous_word_start(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
mut point: DisplayPoint,
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
) -> DisplayPoint {
|
||||||
|
// This works even though find_preceding_boundary is called for every character in the line containing
|
||||||
|
// cursor because the newline is checked only once.
|
||||||
|
point = movement::find_preceding_boundary(map, point, |left, right| {
|
||||||
|
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||||
|
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||||
|
|
||||||
|
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
|
||||||
|
});
|
||||||
|
point
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_of_document(map: &DisplaySnapshot) -> DisplayPoint {
|
||||||
|
0usize.to_display_point(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_of_document(map: &DisplaySnapshot) -> DisplayPoint {
|
||||||
|
map.clip_point(map.max_point(), Bias::Left)
|
||||||
|
}
|
@ -1,212 +1,77 @@
|
|||||||
mod g_prefix;
|
use crate::{
|
||||||
|
motion::Motion,
|
||||||
use crate::VimState;
|
state::{Mode, Operator},
|
||||||
use editor::{char_kind, movement, Bias};
|
Vim,
|
||||||
use gpui::{actions, impl_actions, MutableAppContext, ViewContext};
|
};
|
||||||
|
use editor::Bias;
|
||||||
|
use gpui::MutableAppContext;
|
||||||
use language::SelectionGoal;
|
use language::SelectionGoal;
|
||||||
use serde::Deserialize;
|
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||||
struct MoveToNextWordStart(pub bool);
|
Vim::update(cx, |vim, cx| {
|
||||||
|
match vim.state.operator_stack.pop() {
|
||||||
#[derive(Clone, Deserialize)]
|
None => move_cursor(vim, motion, cx),
|
||||||
struct MoveToNextWordEnd(pub bool);
|
Some(Operator::Change) => change_over(vim, motion, cx),
|
||||||
|
Some(Operator::Delete) => delete_over(vim, motion, cx),
|
||||||
#[derive(Clone, Deserialize)]
|
Some(Operator::Namespace(_)) => panic!(
|
||||||
struct MoveToPreviousWordStart(pub bool);
|
"Normal mode recieved motion with namespaced operator. Likely this means an invalid keymap was used"),
|
||||||
|
}
|
||||||
impl_actions!(
|
vim.clear_operator(cx);
|
||||||
vim,
|
|
||||||
[
|
|
||||||
MoveToNextWordStart,
|
|
||||||
MoveToNextWordEnd,
|
|
||||||
MoveToPreviousWordStart,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
actions!(
|
|
||||||
vim,
|
|
||||||
[
|
|
||||||
GPrefix,
|
|
||||||
MoveLeft,
|
|
||||||
MoveDown,
|
|
||||||
MoveUp,
|
|
||||||
MoveRight,
|
|
||||||
MoveToStartOfLine,
|
|
||||||
MoveToEndOfLine,
|
|
||||||
MoveToEnd,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
|
||||||
g_prefix::init(cx);
|
|
||||||
cx.add_action(move_left);
|
|
||||||
cx.add_action(move_down);
|
|
||||||
cx.add_action(move_up);
|
|
||||||
cx.add_action(move_right);
|
|
||||||
cx.add_action(move_to_start_of_line);
|
|
||||||
cx.add_action(move_to_end_of_line);
|
|
||||||
cx.add_action(move_to_end);
|
|
||||||
cx.add_action(move_to_next_word_start);
|
|
||||||
cx.add_action(move_to_next_word_end);
|
|
||||||
cx.add_action(move_to_previous_word_start);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext<Workspace>) {
|
|
||||||
VimState::update_global(cx, |state, cx| {
|
|
||||||
state.update_active_editor(cx, |editor, cx| {
|
|
||||||
editor.move_cursors(cx, |map, mut cursor, _| {
|
|
||||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
|
||||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_down(_: &mut Workspace, _: &MoveDown, cx: &mut ViewContext<Workspace>) {
|
|
||||||
VimState::update_global(cx, |state, cx| {
|
|
||||||
state.update_active_editor(cx, |editor, cx| {
|
|
||||||
editor.move_cursors(cx, movement::down);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_up(_: &mut Workspace, _: &MoveUp, cx: &mut ViewContext<Workspace>) {
|
fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||||
VimState::update_global(cx, |state, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
state.update_active_editor(cx, |editor, cx| {
|
editor.move_cursors(cx, |map, cursor, goal| motion.move_point(map, cursor, goal))
|
||||||
editor.move_cursors(cx, movement::up);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext<Workspace>) {
|
fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||||
VimState::update_global(cx, |state, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
state.update_active_editor(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
editor.move_cursors(cx, |map, mut cursor, _| {
|
// Don't clip at line ends during change operation
|
||||||
*cursor.column_mut() += 1;
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
(map.clip_point(cursor, Bias::Right), SelectionGoal::None)
|
editor.move_selections(cx, |map, selection| motion.expand_selection(map, selection));
|
||||||
});
|
editor.set_clip_at_line_ends(true, cx);
|
||||||
});
|
match motion {
|
||||||
});
|
Motion::Up => editor.insert(&"\n", cx),
|
||||||
}
|
Motion::Down => editor.insert(&"\n", cx),
|
||||||
|
_ => editor.insert(&"", cx),
|
||||||
|
}
|
||||||
|
|
||||||
fn move_to_start_of_line(
|
if let Motion::Up = motion {
|
||||||
_: &mut Workspace,
|
// Position cursor on previous line after change
|
||||||
_: &MoveToStartOfLine,
|
editor.move_cursors(cx, |map, cursor, goal| {
|
||||||
cx: &mut ViewContext<Workspace>,
|
Motion::Up.move_point(map, cursor, goal)
|
||||||
) {
|
|
||||||
VimState::update_global(cx, |state, cx| {
|
|
||||||
state.update_active_editor(cx, |editor, cx| {
|
|
||||||
editor.move_cursors(cx, |map, cursor, _| {
|
|
||||||
(
|
|
||||||
movement::line_beginning(map, cursor, false),
|
|
||||||
SelectionGoal::None,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_to_end_of_line(_: &mut Workspace, _: &MoveToEndOfLine, cx: &mut ViewContext<Workspace>) {
|
|
||||||
VimState::update_global(cx, |state, cx| {
|
|
||||||
state.update_active_editor(cx, |editor, cx| {
|
|
||||||
editor.move_cursors(cx, |map, cursor, _| {
|
|
||||||
(
|
|
||||||
map.clip_point(movement::line_end(map, cursor, false), Bias::Left),
|
|
||||||
SelectionGoal::None,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_to_end(_: &mut Workspace, _: &MoveToEnd, cx: &mut ViewContext<Workspace>) {
|
|
||||||
VimState::update_global(cx, |state, cx| {
|
|
||||||
state.update_active_editor(cx, |editor, cx| {
|
|
||||||
editor.replace_selections_with(cx, |map| map.clip_point(map.max_point(), Bias::Left));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_to_next_word_start(
|
|
||||||
_: &mut Workspace,
|
|
||||||
&MoveToNextWordStart(treat_punctuation_as_word): &MoveToNextWordStart,
|
|
||||||
cx: &mut ViewContext<Workspace>,
|
|
||||||
) {
|
|
||||||
VimState::update_global(cx, |state, cx| {
|
|
||||||
state.update_active_editor(cx, |editor, cx| {
|
|
||||||
editor.move_cursors(cx, |map, mut cursor, _| {
|
|
||||||
let mut crossed_newline = false;
|
|
||||||
cursor = movement::find_boundary(map, cursor, |left, right| {
|
|
||||||
let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
|
|
||||||
let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
|
|
||||||
let at_newline = right == '\n';
|
|
||||||
|
|
||||||
let found = (left_kind != right_kind && !right.is_whitespace())
|
|
||||||
|| (at_newline && crossed_newline)
|
|
||||||
|| (at_newline && left == '\n'); // Prevents skipping repeated empty lines
|
|
||||||
|
|
||||||
if at_newline {
|
|
||||||
crossed_newline = true;
|
|
||||||
}
|
|
||||||
found
|
|
||||||
});
|
});
|
||||||
(cursor, SelectionGoal::None)
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
vim.switch_mode(Mode::Insert, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_to_next_word_end(
|
fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||||
_: &mut Workspace,
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
&MoveToNextWordEnd(treat_punctuation_as_word): &MoveToNextWordEnd,
|
editor.transact(cx, |editor, cx| {
|
||||||
cx: &mut ViewContext<Workspace>,
|
// Don't clip at line ends during delete operation
|
||||||
) {
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
VimState::update_global(cx, |state, cx| {
|
editor.move_selections(cx, |map, selection| motion.expand_selection(map, selection));
|
||||||
state.update_active_editor(cx, |editor, cx| {
|
match motion {
|
||||||
editor.move_cursors(cx, |map, mut cursor, _| {
|
Motion::Up => editor.insert(&"\n", cx),
|
||||||
*cursor.column_mut() += 1;
|
Motion::Down => editor.insert(&"\n", cx),
|
||||||
cursor = movement::find_boundary(map, cursor, |left, right| {
|
_ => editor.insert(&"", cx),
|
||||||
let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
|
}
|
||||||
let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
|
|
||||||
|
|
||||||
left_kind != right_kind && !left.is_whitespace()
|
if let Motion::Up = motion {
|
||||||
|
// Position cursor on previous line after change
|
||||||
|
editor.move_cursors(cx, |map, cursor, goal| {
|
||||||
|
Motion::Up.move_point(map, cursor, goal)
|
||||||
});
|
});
|
||||||
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
|
}
|
||||||
// we have backtraced already
|
// Fixup cursor position after the deletion
|
||||||
if !map
|
editor.set_clip_at_line_ends(true, cx);
|
||||||
.chars_at(cursor)
|
editor.move_selection_heads(cx, |map, head, _| {
|
||||||
.skip(1)
|
(map.clip_point(head, Bias::Left), SelectionGoal::None)
|
||||||
.next()
|
|
||||||
.map(|c| c == '\n')
|
|
||||||
.unwrap_or(true)
|
|
||||||
{
|
|
||||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
|
||||||
}
|
|
||||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_to_previous_word_start(
|
|
||||||
_: &mut Workspace,
|
|
||||||
&MoveToPreviousWordStart(treat_punctuation_as_word): &MoveToPreviousWordStart,
|
|
||||||
cx: &mut ViewContext<Workspace>,
|
|
||||||
) {
|
|
||||||
VimState::update_global(cx, |state, cx| {
|
|
||||||
state.update_active_editor(cx, |editor, cx| {
|
|
||||||
editor.move_cursors(cx, |map, mut cursor, _| {
|
|
||||||
// This works even though find_preceding_boundary is called for every character in the line containing
|
|
||||||
// cursor because the newline is checked only once.
|
|
||||||
cursor = movement::find_preceding_boundary(map, cursor, |left, right| {
|
|
||||||
let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
|
|
||||||
let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
|
|
||||||
|
|
||||||
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
|
|
||||||
});
|
|
||||||
(cursor, SelectionGoal::None)
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -217,7 +82,13 @@ mod test {
|
|||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use util::test::marked_text;
|
use util::test::marked_text;
|
||||||
|
|
||||||
use crate::vim_test_context::VimTestContext;
|
use crate::{
|
||||||
|
state::{
|
||||||
|
Mode::{self, *},
|
||||||
|
Namespace, Operator,
|
||||||
|
},
|
||||||
|
vim_test_context::VimTestContext,
|
||||||
|
};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_hjkl(cx: &mut gpui::TestAppContext) {
|
async fn test_hjkl(cx: &mut gpui::TestAppContext) {
|
||||||
@ -362,7 +233,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset and test ignoring punctuation
|
// Reset and test ignoring punctuation
|
||||||
cx.simulate_keystrokes(&["g", "g"]);
|
cx.simulate_keystrokes(["g", "g"]);
|
||||||
let (_, cursor_offsets) = marked_text(indoc! {"
|
let (_, cursor_offsets) = marked_text(indoc! {"
|
||||||
The |quick-brown
|
The |quick-brown
|
||||||
|
|
|
|
||||||
@ -392,7 +263,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset and test ignoring punctuation
|
// Reset and test ignoring punctuation
|
||||||
cx.simulate_keystrokes(&["g", "g"]);
|
cx.simulate_keystrokes(["g", "g"]);
|
||||||
let (_, cursor_offsets) = marked_text(indoc! {"
|
let (_, cursor_offsets) = marked_text(indoc! {"
|
||||||
Th|e quick-brow|n
|
Th|e quick-brow|n
|
||||||
|
|
||||||
@ -434,4 +305,232 @@ mod test {
|
|||||||
cx.assert_newest_selection_head_offset(cursor_offset);
|
cx.assert_newest_selection_head_offset(cursor_offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true, "").await;
|
||||||
|
|
||||||
|
// Can abort with escape to get back to normal mode
|
||||||
|
cx.simulate_keystroke("g");
|
||||||
|
assert_eq!(cx.mode(), Normal);
|
||||||
|
assert_eq!(
|
||||||
|
cx.active_operator(),
|
||||||
|
Some(Operator::Namespace(Namespace::G))
|
||||||
|
);
|
||||||
|
cx.simulate_keystroke("escape");
|
||||||
|
assert_eq!(cx.mode(), Normal);
|
||||||
|
assert_eq!(cx.active_operator(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
|
||||||
|
let initial_content = indoc! {"
|
||||||
|
The quick
|
||||||
|
|
||||||
|
brown fox jumps
|
||||||
|
over the lazy dog"};
|
||||||
|
let mut cx = VimTestContext::new(cx, true, initial_content).await;
|
||||||
|
|
||||||
|
// Jump to the end to
|
||||||
|
cx.simulate_keystroke("shift-G");
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
The quick
|
||||||
|
|
||||||
|
brown fox jumps
|
||||||
|
over the lazy do|g"});
|
||||||
|
|
||||||
|
// Jump to the start
|
||||||
|
cx.simulate_keystrokes(["g", "g"]);
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
|The quick
|
||||||
|
|
||||||
|
brown fox jumps
|
||||||
|
over the lazy dog"});
|
||||||
|
assert_eq!(cx.mode(), Normal);
|
||||||
|
assert_eq!(cx.active_operator(), None);
|
||||||
|
|
||||||
|
// Repeat action doesn't change
|
||||||
|
cx.simulate_keystrokes(["g", "g"]);
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
|The quick
|
||||||
|
|
||||||
|
brown fox jumps
|
||||||
|
over the lazy dog"});
|
||||||
|
assert_eq!(cx.mode(), Normal);
|
||||||
|
assert_eq!(cx.active_operator(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_change(cx: &mut gpui::TestAppContext) {
|
||||||
|
fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
|
||||||
|
cx.assert_binding(
|
||||||
|
["c", motion],
|
||||||
|
initial_state,
|
||||||
|
Mode::Normal,
|
||||||
|
state_after,
|
||||||
|
Mode::Insert,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let cx = &mut VimTestContext::new(cx, true, "").await;
|
||||||
|
assert("h", "Te|st", "T|st", cx);
|
||||||
|
assert("l", "Te|st", "Te|t", cx);
|
||||||
|
assert("w", "|Test", "|", cx);
|
||||||
|
assert("w", "Te|st", "Te|", cx);
|
||||||
|
assert("w", "Te|st Test", "Te| Test", cx);
|
||||||
|
assert("e", "Te|st Test", "Te| Test", cx);
|
||||||
|
assert("b", "Te|st", "|st", cx);
|
||||||
|
assert("b", "Test Te|st", "Test |st", cx);
|
||||||
|
assert(
|
||||||
|
"w",
|
||||||
|
indoc! {"
|
||||||
|
The quick
|
||||||
|
brown |fox
|
||||||
|
jumps over"},
|
||||||
|
indoc! {"
|
||||||
|
The quick
|
||||||
|
brown |
|
||||||
|
jumps over"},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
"shift-W",
|
||||||
|
indoc! {"
|
||||||
|
The quick
|
||||||
|
brown |fox-fox
|
||||||
|
jumps over"},
|
||||||
|
indoc! {"
|
||||||
|
The quick
|
||||||
|
brown |
|
||||||
|
jumps over"},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
"k",
|
||||||
|
indoc! {"
|
||||||
|
The quick
|
||||||
|
brown |fox"},
|
||||||
|
indoc! {"
|
||||||
|
|
|
||||||
|
"},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
"j",
|
||||||
|
indoc! {"
|
||||||
|
The q|uick
|
||||||
|
brown fox"},
|
||||||
|
indoc! {"
|
||||||
|
|
||||||
|
|"},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
"shift-$",
|
||||||
|
indoc! {"
|
||||||
|
The q|uick
|
||||||
|
brown fox"},
|
||||||
|
indoc! {"
|
||||||
|
The q|
|
||||||
|
brown fox"},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
"0",
|
||||||
|
indoc! {"
|
||||||
|
The q|uick
|
||||||
|
brown fox"},
|
||||||
|
indoc! {"
|
||||||
|
|uick
|
||||||
|
brown fox"},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_delete(cx: &mut gpui::TestAppContext) {
|
||||||
|
fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
|
||||||
|
cx.assert_binding(
|
||||||
|
["d", motion],
|
||||||
|
initial_state,
|
||||||
|
Mode::Normal,
|
||||||
|
state_after,
|
||||||
|
Mode::Normal,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let cx = &mut VimTestContext::new(cx, true, "").await;
|
||||||
|
assert("h", "Te|st", "T|st", cx);
|
||||||
|
assert("l", "Te|st", "Te|t", cx);
|
||||||
|
assert("w", "|Test", "|", cx);
|
||||||
|
assert("w", "Te|st", "T|e", cx);
|
||||||
|
assert("w", "Te|st Test", "Te|Test", cx);
|
||||||
|
assert("e", "Te|st Test", "Te| Test", cx);
|
||||||
|
assert("b", "Te|st", "|st", cx);
|
||||||
|
assert("b", "Test Te|st", "Test |st", cx);
|
||||||
|
assert(
|
||||||
|
"w",
|
||||||
|
indoc! {"
|
||||||
|
The quick
|
||||||
|
brown |fox
|
||||||
|
jumps over"},
|
||||||
|
// Trailing space after cursor
|
||||||
|
indoc! {"
|
||||||
|
The quick
|
||||||
|
brown|
|
||||||
|
jumps over"},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
"shift-W",
|
||||||
|
indoc! {"
|
||||||
|
The quick
|
||||||
|
brown |fox-fox
|
||||||
|
jumps over"},
|
||||||
|
// Trailing space after cursor
|
||||||
|
indoc! {"
|
||||||
|
The quick
|
||||||
|
brown|
|
||||||
|
jumps over"},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
"k",
|
||||||
|
indoc! {"
|
||||||
|
The quick
|
||||||
|
brown |fox"},
|
||||||
|
indoc! {"
|
||||||
|
|
|
||||||
|
"},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
"j",
|
||||||
|
indoc! {"
|
||||||
|
The q|uick
|
||||||
|
brown fox"},
|
||||||
|
indoc! {"
|
||||||
|
|
||||||
|
|"},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
"shift-$",
|
||||||
|
indoc! {"
|
||||||
|
The q|uick
|
||||||
|
brown fox"},
|
||||||
|
indoc! {"
|
||||||
|
The |q
|
||||||
|
brown fox"},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
"0",
|
||||||
|
indoc! {"
|
||||||
|
The q|uick
|
||||||
|
brown fox"},
|
||||||
|
indoc! {"
|
||||||
|
|uick
|
||||||
|
brown fox"},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
use crate::{mode::Mode, SwitchMode, VimState};
|
|
||||||
use gpui::{actions, MutableAppContext, ViewContext};
|
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
actions!(vim, [MoveToStart]);
|
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
|
||||||
cx.add_action(move_to_start);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_to_start(_: &mut Workspace, _: &MoveToStart, cx: &mut ViewContext<Workspace>) {
|
|
||||||
VimState::update_global(cx, |state, cx| {
|
|
||||||
state.update_active_editor(cx, |editor, cx| {
|
|
||||||
editor.move_to_beginning(&editor::MoveToBeginning, cx);
|
|
||||||
});
|
|
||||||
state.switch_mode(&SwitchMode(Mode::normal()), cx);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use indoc::indoc;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
mode::{Mode, NormalState},
|
|
||||||
vim_test_context::VimTestContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
|
|
||||||
let mut cx = VimTestContext::new(cx, true, "").await;
|
|
||||||
|
|
||||||
// Can abort with escape to get back to normal mode
|
|
||||||
cx.simulate_keystroke("g");
|
|
||||||
assert_eq!(cx.mode(), Mode::Normal(NormalState::GPrefix));
|
|
||||||
cx.simulate_keystroke("escape");
|
|
||||||
assert_eq!(cx.mode(), Mode::normal());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
|
|
||||||
let initial_content = indoc! {"
|
|
||||||
The quick
|
|
||||||
|
|
||||||
brown fox jumps
|
|
||||||
over the lazy dog"};
|
|
||||||
let mut cx = VimTestContext::new(cx, true, initial_content).await;
|
|
||||||
|
|
||||||
// Jump to the end to
|
|
||||||
cx.simulate_keystroke("shift-G");
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
The quick
|
|
||||||
|
|
||||||
brown fox jumps
|
|
||||||
over the lazy do|g"});
|
|
||||||
|
|
||||||
// Jump to the start
|
|
||||||
cx.simulate_keystrokes(&["g", "g"]);
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
|The quick
|
|
||||||
|
|
||||||
brown fox jumps
|
|
||||||
over the lazy dog"});
|
|
||||||
assert_eq!(cx.mode(), Mode::normal());
|
|
||||||
|
|
||||||
// Repeat action doesn't change
|
|
||||||
cx.simulate_keystrokes(&["g", "g"]);
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
|The quick
|
|
||||||
|
|
||||||
brown fox jumps
|
|
||||||
over the lazy dog"});
|
|
||||||
assert_eq!(cx.mode(), Mode::normal());
|
|
||||||
}
|
|
||||||
}
|
|
82
crates/vim/src/state.rs
Normal file
82
crates/vim/src/state.rs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
use editor::CursorShape;
|
||||||
|
use gpui::keymap::Context;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||||
|
pub enum Mode {
|
||||||
|
Normal,
|
||||||
|
Insert,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Mode {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
|
pub enum Namespace {
|
||||||
|
G,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
|
pub enum Operator {
|
||||||
|
Namespace(Namespace),
|
||||||
|
Change,
|
||||||
|
Delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct VimState {
|
||||||
|
pub mode: Mode,
|
||||||
|
pub operator_stack: Vec<Operator>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VimState {
|
||||||
|
pub fn cursor_shape(&self) -> CursorShape {
|
||||||
|
match self.mode {
|
||||||
|
Mode::Normal => CursorShape::Block,
|
||||||
|
Mode::Insert => CursorShape::Bar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vim_controlled(&self) -> bool {
|
||||||
|
!matches!(self.mode, Mode::Insert)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keymap_context_layer(&self) -> Context {
|
||||||
|
let mut context = Context::default();
|
||||||
|
context.map.insert(
|
||||||
|
"vim_mode".to_string(),
|
||||||
|
match self.mode {
|
||||||
|
Mode::Normal => "normal",
|
||||||
|
Mode::Insert => "insert",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.vim_controlled() {
|
||||||
|
context.set.insert("VimControl".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(operator) = &self.operator_stack.last() {
|
||||||
|
operator.set_context(&mut context);
|
||||||
|
}
|
||||||
|
context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operator {
|
||||||
|
pub fn set_context(&self, context: &mut Context) {
|
||||||
|
let operator_context = match self {
|
||||||
|
Operator::Namespace(Namespace::G) => "g",
|
||||||
|
Operator::Change => "c",
|
||||||
|
Operator::Delete => "d",
|
||||||
|
}
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
context
|
||||||
|
.map
|
||||||
|
.insert("vim_operator".to_string(), operator_context.to_string());
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
mod editor_events;
|
mod editor_events;
|
||||||
mod insert;
|
mod insert;
|
||||||
mod mode;
|
mod motion;
|
||||||
mod normal;
|
mod normal;
|
||||||
|
mod state;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod vim_test_context;
|
mod vim_test_context;
|
||||||
|
|
||||||
@ -10,41 +11,53 @@ use editor::{CursorShape, Editor};
|
|||||||
use gpui::{impl_actions, MutableAppContext, ViewContext, WeakViewHandle};
|
use gpui::{impl_actions, MutableAppContext, ViewContext, WeakViewHandle};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use mode::Mode;
|
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
use state::{Mode, Operator, VimState};
|
||||||
use workspace::{self, Workspace};
|
use workspace::{self, Workspace};
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
pub struct SwitchMode(pub Mode);
|
pub struct SwitchMode(pub Mode);
|
||||||
|
|
||||||
impl_actions!(vim, [SwitchMode]);
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct PushOperator(pub Operator);
|
||||||
|
|
||||||
|
impl_actions!(vim, [SwitchMode, PushOperator]);
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
editor_events::init(cx);
|
editor_events::init(cx);
|
||||||
insert::init(cx);
|
insert::init(cx);
|
||||||
normal::init(cx);
|
motion::init(cx);
|
||||||
|
|
||||||
cx.add_action(|_: &mut Workspace, action: &SwitchMode, cx| {
|
cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
|
||||||
VimState::update_global(cx, |state, cx| state.switch_mode(action, cx))
|
Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx))
|
||||||
});
|
});
|
||||||
|
cx.add_action(
|
||||||
|
|_: &mut Workspace, &PushOperator(operator): &PushOperator, cx| {
|
||||||
|
Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
cx.observe_global::<Settings, _>(|settings, cx| {
|
cx.observe_global::<Settings, _>(|settings, cx| {
|
||||||
VimState::update_global(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
|
Vim::update(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct VimState {
|
pub struct Vim {
|
||||||
editors: HashMap<usize, WeakViewHandle<Editor>>,
|
editors: HashMap<usize, WeakViewHandle<Editor>>,
|
||||||
active_editor: Option<WeakViewHandle<Editor>>,
|
active_editor: Option<WeakViewHandle<Editor>>,
|
||||||
|
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
mode: Mode,
|
state: VimState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VimState {
|
impl Vim {
|
||||||
fn update_global<F, S>(cx: &mut MutableAppContext, update: F) -> S
|
fn read(cx: &mut MutableAppContext) -> &Self {
|
||||||
|
cx.default_global()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update<F, S>(cx: &mut MutableAppContext, update: F) -> S
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut Self, &mut MutableAppContext) -> S,
|
F: FnOnce(&mut Self, &mut MutableAppContext) -> S,
|
||||||
{
|
{
|
||||||
@ -62,33 +75,54 @@ impl VimState {
|
|||||||
.map(|ae| ae.update(cx, update))
|
.map(|ae| ae.update(cx, update))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn switch_mode(&mut self, SwitchMode(mode): &SwitchMode, cx: &mut MutableAppContext) {
|
fn switch_mode(&mut self, mode: Mode, cx: &mut MutableAppContext) {
|
||||||
self.mode = *mode;
|
self.state.mode = mode;
|
||||||
|
self.state.operator_stack.clear();
|
||||||
self.sync_editor_options(cx);
|
self.sync_editor_options(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_operator(&mut self, operator: Operator, cx: &mut MutableAppContext) {
|
||||||
|
self.state.operator_stack.push(operator);
|
||||||
|
self.sync_editor_options(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator {
|
||||||
|
let popped_operator = self.state.operator_stack.pop().expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
|
||||||
|
self.sync_editor_options(cx);
|
||||||
|
popped_operator
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_operator(&mut self, cx: &mut MutableAppContext) {
|
||||||
|
self.state.operator_stack.clear();
|
||||||
|
self.sync_editor_options(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn active_operator(&mut self) -> Option<Operator> {
|
||||||
|
self.state.operator_stack.last().copied()
|
||||||
|
}
|
||||||
|
|
||||||
fn set_enabled(&mut self, enabled: bool, cx: &mut MutableAppContext) {
|
fn set_enabled(&mut self, enabled: bool, cx: &mut MutableAppContext) {
|
||||||
if self.enabled != enabled {
|
if self.enabled != enabled {
|
||||||
self.enabled = enabled;
|
self.enabled = enabled;
|
||||||
self.mode = Default::default();
|
self.state = Default::default();
|
||||||
if enabled {
|
if enabled {
|
||||||
self.mode = Mode::normal();
|
self.state.mode = Mode::Normal;
|
||||||
}
|
}
|
||||||
self.sync_editor_options(cx);
|
self.sync_editor_options(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_editor_options(&self, cx: &mut MutableAppContext) {
|
fn sync_editor_options(&self, cx: &mut MutableAppContext) {
|
||||||
let mode = self.mode;
|
let state = &self.state;
|
||||||
let cursor_shape = mode.cursor_shape();
|
let cursor_shape = state.cursor_shape();
|
||||||
for editor in self.editors.values() {
|
for editor in self.editors.values() {
|
||||||
if let Some(editor) = editor.upgrade(cx) {
|
if let Some(editor) = editor.upgrade(cx) {
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
if self.enabled {
|
if self.enabled {
|
||||||
editor.set_cursor_shape(cursor_shape, cx);
|
editor.set_cursor_shape(cursor_shape, cx);
|
||||||
editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx);
|
editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx);
|
||||||
editor.set_input_enabled(mode == Mode::Insert);
|
editor.set_input_enabled(!state.vim_controlled());
|
||||||
let context_layer = mode.keymap_context_layer();
|
let context_layer = state.keymap_context_layer();
|
||||||
editor.set_keymap_context_layer::<Self>(context_layer);
|
editor.set_keymap_context_layer::<Self>(context_layer);
|
||||||
} else {
|
} else {
|
||||||
editor.set_cursor_shape(CursorShape::Bar, cx);
|
editor.set_cursor_shape(CursorShape::Bar, cx);
|
||||||
@ -104,12 +138,12 @@ impl VimState {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::{mode::Mode, vim_test_context::VimTestContext};
|
use crate::{state::Mode, vim_test_context::VimTestContext};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = VimTestContext::new(cx, false, "").await;
|
let mut cx = VimTestContext::new(cx, false, "").await;
|
||||||
cx.simulate_keystrokes(&["h", "j", "k", "l"]);
|
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||||
cx.assert_editor_state("hjkl|");
|
cx.assert_editor_state("hjkl|");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,22 +156,22 @@ mod test {
|
|||||||
|
|
||||||
// Editor acts as though vim is disabled
|
// Editor acts as though vim is disabled
|
||||||
cx.disable_vim();
|
cx.disable_vim();
|
||||||
cx.simulate_keystrokes(&["h", "j", "k", "l"]);
|
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||||
cx.assert_editor_state("hjkl|");
|
cx.assert_editor_state("hjkl|");
|
||||||
|
|
||||||
// Enabling dynamically sets vim mode again and restores normal mode
|
// Enabling dynamically sets vim mode again and restores normal mode
|
||||||
cx.enable_vim();
|
cx.enable_vim();
|
||||||
assert_eq!(cx.mode(), Mode::normal());
|
assert_eq!(cx.mode(), Mode::Normal);
|
||||||
cx.simulate_keystrokes(&["h", "h", "h", "l"]);
|
cx.simulate_keystrokes(["h", "h", "h", "l"]);
|
||||||
assert_eq!(cx.editor_text(), "hjkl".to_owned());
|
assert_eq!(cx.editor_text(), "hjkl".to_owned());
|
||||||
cx.assert_editor_state("hj|kl");
|
cx.assert_editor_state("hj|kl");
|
||||||
cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]);
|
cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
|
||||||
cx.assert_editor_state("hjTest|kl");
|
cx.assert_editor_state("hjTest|kl");
|
||||||
|
|
||||||
// Disabling and enabling resets to normal mode
|
// Disabling and enabling resets to normal mode
|
||||||
assert_eq!(cx.mode(), Mode::Insert);
|
assert_eq!(cx.mode(), Mode::Insert);
|
||||||
cx.disable_vim();
|
cx.disable_vim();
|
||||||
cx.enable_vim();
|
cx.enable_vim();
|
||||||
assert_eq!(cx.mode(), Mode::normal());
|
assert_eq!(cx.mode(), Mode::Normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use language::{Point, Selection};
|
|||||||
use util::test::marked_text;
|
use util::test::marked_text;
|
||||||
use workspace::{WorkspaceHandle, WorkspaceParams};
|
use workspace::{WorkspaceHandle, WorkspaceParams};
|
||||||
|
|
||||||
use crate::*;
|
use crate::{state::Operator, *};
|
||||||
|
|
||||||
pub struct VimTestContext<'a> {
|
pub struct VimTestContext<'a> {
|
||||||
cx: &'a mut gpui::TestAppContext,
|
cx: &'a mut gpui::TestAppContext,
|
||||||
@ -100,7 +100,12 @@ impl<'a> VimTestContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn mode(&mut self) -> Mode {
|
pub fn mode(&mut self) -> Mode {
|
||||||
self.cx.update(|cx| cx.global::<VimState>().mode)
|
self.cx.read(|cx| cx.global::<Vim>().state.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active_operator(&mut self) -> Option<Operator> {
|
||||||
|
self.cx
|
||||||
|
.read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn editor_text(&mut self) -> String {
|
pub fn editor_text(&mut self) -> String {
|
||||||
@ -119,12 +124,23 @@ impl<'a> VimTestContext<'a> {
|
|||||||
.dispatch_keystroke(self.window_id, keystroke, input, false);
|
.dispatch_keystroke(self.window_id, keystroke, input, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) {
|
pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
|
||||||
for keystroke_text in keystroke_texts.into_iter() {
|
for keystroke_text in keystroke_texts.into_iter() {
|
||||||
self.simulate_keystroke(keystroke_text);
|
self.simulate_keystroke(keystroke_text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_state(&mut self, text: &str, mode: Mode) {
|
||||||
|
self.cx
|
||||||
|
.update(|cx| Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx)));
|
||||||
|
self.editor.update(self.cx, |editor, cx| {
|
||||||
|
let (unmarked_text, markers) = marked_text(&text);
|
||||||
|
editor.set_text(unmarked_text, cx);
|
||||||
|
let cursor_offset = markers[0];
|
||||||
|
editor.replace_selections_with(cx, |map| cursor_offset.to_display_point(map));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn assert_newest_selection_head_offset(&mut self, expected_offset: usize) {
|
pub fn assert_newest_selection_head_offset(&mut self, expected_offset: usize) {
|
||||||
let actual_head = self.newest_selection().head();
|
let actual_head = self.newest_selection().head();
|
||||||
let (actual_offset, expected_head) = self.editor.update(self.cx, |editor, cx| {
|
let (actual_offset, expected_head) = self.editor.update(self.cx, |editor, cx| {
|
||||||
@ -171,6 +187,21 @@ impl<'a> VimTestContext<'a> {
|
|||||||
actual_position_text, expected_position_text
|
actual_position_text, expected_position_text
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn assert_binding<const COUNT: usize>(
|
||||||
|
&mut self,
|
||||||
|
keystrokes: [&str; COUNT],
|
||||||
|
initial_state: &str,
|
||||||
|
initial_mode: Mode,
|
||||||
|
state_after: &str,
|
||||||
|
mode_after: Mode,
|
||||||
|
) {
|
||||||
|
self.set_state(initial_state, initial_mode);
|
||||||
|
self.simulate_keystrokes(keystrokes);
|
||||||
|
self.assert_editor_state(state_after);
|
||||||
|
assert_eq!(self.mode(), mode_after);
|
||||||
|
assert_eq!(self.active_operator(), None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Deref for VimTestContext<'a> {
|
impl<'a> Deref for VimTestContext<'a> {
|
||||||
|
Loading…
Reference in New Issue
Block a user