Closes #4642 - Added the ability to switch to helix normal mode, with an additional helix visual mode. - <kbd>ctrl</kbd><kbd>h</kbd> from Insert mode goes to Helix Normal mode. <kbd> i </kbd> and <kbd> a </kbd> to go back. - Need to find a way to perform the helix normal mode selection with <kbd> w </kbd>, <kbd>e </kbd>, <kbd> b </kbd> as a first step. Need to figure out how the mode will interoperate with the VIM mode as the new additions are in the same crate.
272 lines
9.6 KiB
Rust
272 lines
9.6 KiB
Rust
use editor::{movement, scroll::Autoscroll, DisplayPoint, Editor};
|
|
use gpui::{actions, Action};
|
|
use language::{CharClassifier, CharKind};
|
|
use ui::ViewContext;
|
|
|
|
use crate::{motion::Motion, state::Mode, Vim};
|
|
|
|
actions!(vim, [HelixNormalAfter]);
|
|
|
|
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
|
|
Vim::action(editor, cx, Vim::helix_normal_after);
|
|
}
|
|
|
|
impl Vim {
|
|
pub fn helix_normal_after(&mut self, action: &HelixNormalAfter, cx: &mut ViewContext<Self>) {
|
|
if self.active_operator().is_some() {
|
|
self.operator_stack.clear();
|
|
self.sync_vim_settings(cx);
|
|
return;
|
|
}
|
|
self.stop_recording_immediately(action.boxed_clone(), cx);
|
|
self.switch_mode(Mode::HelixNormal, false, cx);
|
|
return;
|
|
}
|
|
|
|
pub fn helix_normal_motion(
|
|
&mut self,
|
|
motion: Motion,
|
|
times: Option<usize>,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
self.helix_move_cursor(motion, times, cx);
|
|
}
|
|
|
|
fn helix_find_range_forward(
|
|
&mut self,
|
|
times: Option<usize>,
|
|
cx: &mut ViewContext<Self>,
|
|
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
|
|
) {
|
|
self.update_editor(cx, |_, editor, cx| {
|
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
|
s.move_with(|map, selection| {
|
|
let times = times.unwrap_or(1);
|
|
|
|
if selection.head() == map.max_point() {
|
|
return;
|
|
}
|
|
|
|
// collapse to block cursor
|
|
if selection.tail() < selection.head() {
|
|
selection.set_tail(movement::left(map, selection.head()), selection.goal);
|
|
} else {
|
|
selection.set_tail(selection.head(), selection.goal);
|
|
selection.set_head(movement::right(map, selection.head()), selection.goal);
|
|
}
|
|
|
|
// create a classifier
|
|
let classifier = map
|
|
.buffer_snapshot
|
|
.char_classifier_at(selection.head().to_point(map));
|
|
|
|
let mut last_selection = selection.clone();
|
|
for _ in 0..times {
|
|
let (new_tail, new_head) =
|
|
movement::find_boundary_trail(map, selection.head(), |left, right| {
|
|
is_boundary(left, right, &classifier)
|
|
});
|
|
|
|
selection.set_head(new_head, selection.goal);
|
|
if let Some(new_tail) = new_tail {
|
|
selection.set_tail(new_tail, selection.goal);
|
|
}
|
|
|
|
if selection.head() == last_selection.head()
|
|
&& selection.tail() == last_selection.tail()
|
|
{
|
|
break;
|
|
}
|
|
last_selection = selection.clone();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
fn helix_find_range_backward(
|
|
&mut self,
|
|
times: Option<usize>,
|
|
cx: &mut ViewContext<Self>,
|
|
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
|
|
) {
|
|
self.update_editor(cx, |_, editor, cx| {
|
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
|
s.move_with(|map, selection| {
|
|
let times = times.unwrap_or(1);
|
|
|
|
if selection.head() == DisplayPoint::zero() {
|
|
return;
|
|
}
|
|
|
|
// collapse to block cursor
|
|
if selection.tail() < selection.head() {
|
|
selection.set_tail(movement::left(map, selection.head()), selection.goal);
|
|
} else {
|
|
selection.set_tail(selection.head(), selection.goal);
|
|
selection.set_head(movement::right(map, selection.head()), selection.goal);
|
|
}
|
|
|
|
// flip the selection
|
|
selection.swap_head_tail();
|
|
|
|
// create a classifier
|
|
let classifier = map
|
|
.buffer_snapshot
|
|
.char_classifier_at(selection.head().to_point(map));
|
|
|
|
let mut last_selection = selection.clone();
|
|
for _ in 0..times {
|
|
let (new_tail, new_head) = movement::find_preceding_boundary_trail(
|
|
map,
|
|
selection.head(),
|
|
|left, right| is_boundary(left, right, &classifier),
|
|
);
|
|
|
|
selection.set_head(new_head, selection.goal);
|
|
if let Some(new_tail) = new_tail {
|
|
selection.set_tail(new_tail, selection.goal);
|
|
}
|
|
|
|
if selection.head() == last_selection.head()
|
|
&& selection.tail() == last_selection.tail()
|
|
{
|
|
break;
|
|
}
|
|
last_selection = selection.clone();
|
|
}
|
|
});
|
|
})
|
|
});
|
|
}
|
|
|
|
pub fn helix_move_and_collapse(
|
|
&mut self,
|
|
motion: Motion,
|
|
times: Option<usize>,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
self.update_editor(cx, |_, editor, cx| {
|
|
let text_layout_details = editor.text_layout_details(cx);
|
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
|
s.move_with(|map, selection| {
|
|
let goal = selection.goal;
|
|
let cursor = if selection.is_empty() || selection.reversed {
|
|
selection.head()
|
|
} else {
|
|
movement::left(map, selection.head())
|
|
};
|
|
|
|
let (point, goal) = motion
|
|
.move_point(map, cursor, selection.goal, times, &text_layout_details)
|
|
.unwrap_or((cursor, goal));
|
|
|
|
selection.collapse_to(point, goal)
|
|
})
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn helix_move_cursor(
|
|
&mut self,
|
|
motion: Motion,
|
|
times: Option<usize>,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
match motion {
|
|
Motion::NextWordStart { ignore_punctuation } => {
|
|
self.helix_find_range_forward(times, cx, |left, right, classifier| {
|
|
let left_kind = classifier.kind_with(left, ignore_punctuation);
|
|
let right_kind = classifier.kind_with(right, ignore_punctuation);
|
|
let at_newline = right == '\n';
|
|
|
|
let found =
|
|
left_kind != right_kind && right_kind != CharKind::Whitespace || at_newline;
|
|
|
|
found
|
|
})
|
|
}
|
|
Motion::NextWordEnd { ignore_punctuation } => {
|
|
self.helix_find_range_forward(times, cx, |left, right, classifier| {
|
|
let left_kind = classifier.kind_with(left, ignore_punctuation);
|
|
let right_kind = classifier.kind_with(right, ignore_punctuation);
|
|
let at_newline = right == '\n';
|
|
|
|
let found = left_kind != right_kind
|
|
&& (left_kind != CharKind::Whitespace || at_newline);
|
|
|
|
found
|
|
})
|
|
}
|
|
Motion::PreviousWordStart { ignore_punctuation } => {
|
|
self.helix_find_range_backward(times, cx, |left, right, classifier| {
|
|
let left_kind = classifier.kind_with(left, ignore_punctuation);
|
|
let right_kind = classifier.kind_with(right, ignore_punctuation);
|
|
let at_newline = right == '\n';
|
|
|
|
let found = left_kind != right_kind
|
|
&& (left_kind != CharKind::Whitespace || at_newline);
|
|
|
|
found
|
|
})
|
|
}
|
|
Motion::PreviousWordEnd { ignore_punctuation } => {
|
|
self.helix_find_range_backward(times, cx, |left, right, classifier| {
|
|
let left_kind = classifier.kind_with(left, ignore_punctuation);
|
|
let right_kind = classifier.kind_with(right, ignore_punctuation);
|
|
let at_newline = right == '\n';
|
|
|
|
let found = left_kind != right_kind
|
|
&& right_kind != CharKind::Whitespace
|
|
&& !at_newline;
|
|
|
|
found
|
|
})
|
|
}
|
|
_ => self.helix_move_and_collapse(motion, times, cx),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use indoc::indoc;
|
|
|
|
use crate::{state::Mode, test::VimTestContext};
|
|
|
|
#[gpui::test]
|
|
async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
// «
|
|
// ˇ
|
|
// »
|
|
cx.set_state(
|
|
indoc! {"
|
|
The quˇick brown
|
|
fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
cx.simulate_keystrokes("w");
|
|
|
|
cx.assert_state(
|
|
indoc! {"
|
|
The qu«ick ˇ»brown
|
|
fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
cx.simulate_keystrokes("w");
|
|
|
|
cx.assert_state(
|
|
indoc! {"
|
|
The quick «brownˇ»
|
|
fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
}
|
|
}
|