working f and t bindings

This commit is contained in:
Kay Simmons 2023-01-06 14:03:01 -08:00
parent 6a57bd2794
commit 73e7967a12
37 changed files with 1143 additions and 860 deletions

View file

@ -3,7 +3,7 @@ use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
movement, Bias, CharKind, DisplayPoint,
};
use gpui::{actions, impl_actions, MutableAppContext};
use gpui::{actions, impl_actions, keymap_matcher::KeyPressed, MutableAppContext};
use language::{Point, Selection, SelectionGoal};
use serde::Deserialize;
use workspace::Workspace;
@ -32,6 +32,8 @@ pub enum Motion {
StartOfDocument,
EndOfDocument,
Matching,
FindForward { before: bool, character: char },
FindBackward { after: bool, character: char },
}
#[derive(Clone, Deserialize, PartialEq)]
@ -107,10 +109,34 @@ pub fn init(cx: &mut MutableAppContext) {
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
);
cx.add_action(
|_: &mut Workspace, KeyPressed { keystroke }: &KeyPressed, cx| match Vim::read(cx)
.active_operator()
{
Some(Operator::FindForward { before }) => motion(
Motion::FindForward {
before,
character: keystroke.key.chars().next().unwrap(),
},
cx,
),
Some(Operator::FindBackward { after }) => motion(
Motion::FindBackward {
after,
character: keystroke.key.chars().next().unwrap(),
},
cx,
),
_ => cx.propagate_action(),
},
)
}
pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
if let Some(Operator::Namespace(_)) = Vim::read(cx).active_operator() {
if let Some(Operator::Namespace(_))
| Some(Operator::FindForward { .. })
| Some(Operator::FindBackward { .. }) = Vim::read(cx).active_operator()
{
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
}
@ -152,14 +178,16 @@ impl Motion {
| CurrentLine
| EndOfLine
| NextWordEnd { .. }
| Matching => true,
| Matching
| FindForward { .. } => true,
Left
| Backspace
| Right
| StartOfLine
| NextWordStart { .. }
| PreviousWordStart { .. }
| FirstNonWhitespace => false,
| FirstNonWhitespace
| FindBackward { .. } => false,
}
}
@ -196,6 +224,14 @@ impl Motion {
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
Matching => (matching(map, point), SelectionGoal::None),
FindForward { before, character } => (
find_forward(map, point, before, character, times),
SelectionGoal::None,
),
FindBackward { after, character } => (
find_backward(map, point, after, character, times),
SelectionGoal::None,
),
};
(new_point != point || self.infallible()).then_some((new_point, goal))
@ -446,3 +482,50 @@ fn matching(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
point
}
}
fn find_forward(
map: &DisplaySnapshot,
from: DisplayPoint,
before: bool,
target: char,
mut times: usize,
) -> DisplayPoint {
let mut previous_point = from;
for (ch, point) in map.chars_at(from) {
if ch == target && point != from {
times -= 1;
if times == 0 {
return if before { previous_point } else { point };
}
} else if ch == '\n' {
break;
}
previous_point = point;
}
from
}
fn find_backward(
map: &DisplaySnapshot,
from: DisplayPoint,
after: bool,
target: char,
mut times: usize,
) -> DisplayPoint {
let mut previous_point = from;
for (ch, point) in map.reverse_chars_at(from) {
if ch == target && point != from {
times -= 1;
if times == 0 {
return if after { previous_point } else { point };
}
} else if ch == '\n' {
break;
}
previous_point = point;
}
from
}

View file

@ -18,6 +18,7 @@ use editor::{
};
use gpui::{actions, impl_actions, MutableAppContext, ViewContext};
use language::{AutoindentMode, Point, SelectionGoal};
use log::error;
use serde::Deserialize;
use workspace::Workspace;
@ -101,8 +102,9 @@ pub fn normal_motion(
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) => {
// Can't do anything for text objects or namespace operators. Ignoring
error!("Unexpected normal mode motion operator: {:?}", operator)
}
}
});
@ -912,4 +914,42 @@ mod test {
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
}
#[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ˇ aˇaaˇbaaa
ˇ ˇbˇaaˇa ˇbˇbˇb
ˇ
ˇb
"};
cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
.await;
cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
.await;
}
}
#[gpui::test]
async fn test_capital_f_and_capital_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ˇ aˇaaˇbaaa
ˇ ˇbˇaaˇa ˇbˇbˇb
ˇ
ˇb
"};
cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
.await;
cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
.await;
}
}
}

View file

@ -1,4 +1,4 @@
use gpui::keymap::Context;
use gpui::keymap_matcher::KeymapContext;
use language::CursorShape;
use serde::{Deserialize, Serialize};
@ -29,6 +29,8 @@ pub enum Operator {
Delete,
Yank,
Object { around: bool },
FindForward { before: bool },
FindBackward { after: bool },
}
#[derive(Default)]
@ -54,6 +56,10 @@ impl VimState {
pub fn vim_controlled(&self) -> bool {
!matches!(self.mode, Mode::Insert)
|| matches!(
self.operator_stack.last(),
Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. })
)
}
pub fn clip_at_line_end(&self) -> bool {
@ -64,8 +70,8 @@ impl VimState {
!matches!(self.mode, Mode::Visual { .. })
}
pub fn keymap_context_layer(&self) -> Context {
let mut context = Context::default();
pub fn keymap_context_layer(&self) -> KeymapContext {
let mut context = KeymapContext::default();
context.map.insert(
"vim_mode".to_string(),
match self.mode {
@ -81,34 +87,48 @@ impl VimState {
}
let active_operator = self.operator_stack.last();
if matches!(active_operator, Some(Operator::Object { .. })) {
context.set.insert("VimObject".to_string());
if let Some(active_operator) = active_operator {
for context_flag in active_operator.context_flags().into_iter() {
context.set.insert(context_flag.to_string());
}
}
Operator::set_context(active_operator, &mut context);
context.map.insert(
"vim_operator".to_string(),
active_operator
.map(|op| op.id())
.unwrap_or_else(|| "none")
.to_string(),
);
context
}
}
impl Operator {
pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
let operator_context = match operator {
Some(Operator::Number(_)) => "n",
Some(Operator::Namespace(Namespace::G)) => "g",
Some(Operator::Namespace(Namespace::Z)) => "z",
Some(Operator::Object { around: false }) => "i",
Some(Operator::Object { around: true }) => "a",
Some(Operator::Change) => "c",
Some(Operator::Delete) => "d",
Some(Operator::Yank) => "y",
None => "none",
pub fn id(&self) -> &'static str {
match self {
Operator::Number(_) => "n",
Operator::Namespace(Namespace::G) => "g",
Operator::Namespace(Namespace::Z) => "z",
Operator::Object { around: false } => "i",
Operator::Object { around: true } => "a",
Operator::Change => "c",
Operator::Delete => "d",
Operator::Yank => "y",
Operator::FindForward { before: false } => "f",
Operator::FindForward { before: true } => "t",
Operator::FindBackward { after: false } => "F",
Operator::FindBackward { after: true } => "T",
}
.to_owned();
}
context
.map
.insert("vim_operator".to_string(), operator_context);
pub fn context_flags(&self) -> &'static [&'static str] {
match self {
Operator::Object { .. } => &["VimObject"],
Operator::FindForward { .. } | Operator::FindBackward { .. } => &["VimWaiting"],
_ => &[],
}
}
}

View file

@ -7,7 +7,7 @@ use async_compat::Compat;
#[cfg(feature = "neovim")]
use async_trait::async_trait;
#[cfg(feature = "neovim")]
use gpui::keymap::Keystroke;
use gpui::keymap_matcher::Keystroke;
use language::{Point, Selection};

View file

@ -16,7 +16,6 @@ use editor::{Bias, Cancel, Editor};
use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
use language::CursorShape;
use serde::Deserialize;
use settings::Settings;
use state::{Mode, Operator, VimState};
use workspace::{self, Workspace};
@ -55,7 +54,7 @@ pub fn init(cx: &mut MutableAppContext) {
// Editor Actions
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
// If we are in a non normal mode or have an active operator, swap to normal mode
// If we are in aren't in normal mode or have an active operator, swap to normal mode
// Otherwise forward cancel on to the editor
let vim = Vim::read(cx);
if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
@ -81,17 +80,21 @@ pub fn init(cx: &mut MutableAppContext) {
.detach();
}
// Any keystrokes not mapped to vim should clear the active operator
pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) {
cx.observe_keystrokes(window_id, |_keystroke, _result, handled_by, cx| {
if let Some(handled_by) = handled_by {
if handled_by.namespace() == "vim" {
// Keystroke is handled by the vim system, so continue forward
// Also short circuit if it is the special cancel action
if handled_by.namespace() == "vim"
|| (handled_by.namespace() == "editor" && handled_by.name() == "Cancel")
{
return true;
}
}
Vim::update(cx, |vim, cx| {
if vim.active_operator().is_some() {
// If the keystroke is not handled by vim, we should clear the operator
vim.clear_operator(cx);
}
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long