Implement Helix Support (WIP) (#19175)
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.
This commit is contained in:
parent
c5d15fd065
commit
8f08787cf0
11 changed files with 444 additions and 12 deletions
271
crates/vim/src/helix.rs
Normal file
271
crates/vim/src/helix.rs
Normal file
|
@ -0,0 +1,271 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue