diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 99c94798db..bef6f48cb4 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -1,6 +1,6 @@ [ { - "context": "Editor && VimControl", + "context": "Editor && VimControl && !VimWaiting", "bindings": { "g": [ "vim::PushOperator", @@ -53,6 +53,42 @@ } ], "%": "vim::Matching", + "ctrl-y": [ + "vim::Scroll", + "LineUp" + ], + "f": [ + "vim::PushOperator", + { + "FindForward": { + "before": false + } + } + ], + "t": [ + "vim::PushOperator", + { + "FindForward": { + "before": true + } + } + ], + "shift-f": [ + "vim::PushOperator", + { + "FindBackward": { + "after": false + } + } + ], + "shift-t": [ + "vim::PushOperator", + { + "FindBackward": { + "after": true + } + } + ], "escape": "editor::Cancel", "0": "vim::StartOfLine", // When no number operator present, use start of line motion "1": [ @@ -94,7 +130,7 @@ } }, { - "context": "Editor && vim_mode == normal && vim_operator == none", + "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", "bindings": { "c": [ "vim::PushOperator", @@ -173,10 +209,6 @@ "ctrl-e": [ "vim::Scroll", "LineDown" - ], - "ctrl-y": [ - "vim::Scroll", - "LineUp" ] } }, @@ -255,7 +287,7 @@ } }, { - "context": "Editor && vim_mode == visual", + "context": "Editor && vim_mode == visual && !VimWaiting", "bindings": { "u": "editor::Undo", "c": "vim::VisualChange", @@ -271,5 +303,11 @@ "escape": "vim::NormalBefore", "ctrl-c": "vim::NormalBefore" } + }, + { + "context": "Editor && VimWaiting", + "bindings": { + "*": "gpui::KeyPressed" + } } ] \ No newline at end of file diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index ba6b236a82..743b98adb0 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -8,8 +8,10 @@ use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, - impl_actions, impl_internal_actions, keymap, AppContext, CursorStyle, Entity, ModelHandle, - MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, + impl_actions, impl_internal_actions, + keymap_matcher::KeymapContext, + AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, + Subscription, View, ViewContext, ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::Project; @@ -1267,7 +1269,7 @@ impl View for ContactList { "ContactList" } - fn keymap_context(&self, _: &AppContext) -> keymap::Context { + fn keymap_context(&self, _: &AppContext) -> KeymapContext { let mut cx = Self::default_keymap_context(); cx.set.insert("menu".into()); cx diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 3742e36c72..d4625cbce0 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -3,7 +3,7 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, elements::{ChildView, Flex, Label, ParentElement}, - keymap::Keystroke, + keymap_matcher::Keystroke, Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; @@ -64,8 +64,10 @@ impl CommandPalette { name: humanize_action_name(name), action, keystrokes: bindings + .iter() + .filter_map(|binding| binding.keystrokes()) .last() - .map_or(Vec::new(), |binding| binding.keystrokes().to_vec()), + .map_or(Vec::new(), |keystrokes| keystrokes.to_vec()), }) }) .collect(); diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 96d99f5109..0dc0ce6f42 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -1,7 +1,7 @@ use gpui::{ - elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle, - Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton, MutableAppContext, RenderContext, - SizeConstraint, Subscription, View, ViewContext, + elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext, + platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton, + MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext, }; use menu::*; use settings::Settings; @@ -75,7 +75,7 @@ impl View for ContextMenu { "ContextMenu" } - fn keymap_context(&self, _: &AppContext) -> keymap::Context { + fn keymap_context(&self, _: &AppContext) -> KeymapContext { let mut cx = Self::default_keymap_context(); cx.set.insert("menu".into()); cx diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d8ee49866b..535bd4a57c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -36,6 +36,7 @@ use gpui::{ fonts::{self, HighlightStyle, TextStyle}, geometry::vector::Vector2F, impl_actions, impl_internal_actions, + keymap_matcher::KeymapContext, platform::CursorStyle, serde_json::json, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, @@ -464,7 +465,7 @@ pub struct Editor { searchable: bool, cursor_shape: CursorShape, workspace_id: Option, - keymap_context_layers: BTreeMap, + keymap_context_layers: BTreeMap, input_enabled: bool, leader_replica_id: Option, remote_id: Option, @@ -1225,7 +1226,7 @@ impl Editor { } } - pub fn set_keymap_context_layer(&mut self, context: gpui::keymap::Context) { + pub fn set_keymap_context_layer(&mut self, context: KeymapContext) { self.keymap_context_layers .insert(TypeId::of::(), context); } @@ -6245,7 +6246,7 @@ impl View for Editor { false } - fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context { + fn keymap_context(&self, _: &AppContext) -> KeymapContext { let mut context = Self::default_keymap_context(); let mode = match self.mode { EditorMode::SingleLine => "single_line", diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 568f29d3e1..d8dbfee171 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -9,7 +9,9 @@ use indoc::indoc; use crate::{ display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, }; -use gpui::{keymap::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle}; +use gpui::{ + keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle, +}; use language::{Buffer, BufferSnapshot}; use settings::Settings; use util::{ diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 5568155cf7..6b784833c7 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -28,7 +28,6 @@ use smol::prelude::*; pub use action::*; use callback_collection::CallbackCollection; use collections::{hash_map::Entry, HashMap, HashSet, VecDeque}; -use keymap::MatchResult; use platform::Event; #[cfg(any(test, feature = "test-support"))] pub use test_app_context::{ContextHandle, TestAppContext}; @@ -37,7 +36,7 @@ use crate::{ elements::ElementBox, executor::{self, Task}, geometry::rect::RectF, - keymap::{self, Binding, Keystroke}, + keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult}, platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::post_inc, @@ -72,11 +71,11 @@ pub trait View: Entity + Sized { false } - fn keymap_context(&self, _: &AppContext) -> keymap::Context { + fn keymap_context(&self, _: &AppContext) -> keymap_matcher::KeymapContext { Self::default_keymap_context() } - fn default_keymap_context() -> keymap::Context { - let mut cx = keymap::Context::default(); + fn default_keymap_context() -> keymap_matcher::KeymapContext { + let mut cx = keymap_matcher::KeymapContext::default(); cx.set.insert(Self::ui_name().into()); cx } @@ -609,7 +608,7 @@ pub struct MutableAppContext { capture_actions: HashMap>>>, actions: HashMap>>>, global_actions: HashMap>, - keystroke_matcher: keymap::Matcher, + keystroke_matcher: KeymapMatcher, next_entity_id: usize, next_window_id: usize, next_subscription_id: usize, @@ -668,7 +667,7 @@ impl MutableAppContext { capture_actions: Default::default(), actions: Default::default(), global_actions: Default::default(), - keystroke_matcher: keymap::Matcher::default(), + keystroke_matcher: KeymapMatcher::default(), next_entity_id: 0, next_window_id: 0, next_subscription_id: 0, @@ -1361,8 +1360,10 @@ impl MutableAppContext { .views .get(&(window_id, *view_id)) .expect("view in responder chain does not exist"); - let cx = view.keymap_context(self.as_ref()); - let keystrokes = self.keystroke_matcher.keystrokes_for_action(action, &cx); + let keymap_context = view.keymap_context(self.as_ref()); + let keystrokes = self + .keystroke_matcher + .keystrokes_for_action(action, &keymap_context); if keystrokes.is_some() { return keystrokes; } @@ -1443,7 +1444,7 @@ impl MutableAppContext { }) } - pub fn add_bindings>(&mut self, bindings: T) { + pub fn add_bindings>(&mut self, bindings: T) { self.keystroke_matcher.add_bindings(bindings); } @@ -3139,7 +3140,7 @@ pub trait AnyView { window_id: usize, view_id: usize, ) -> bool; - fn keymap_context(&self, cx: &AppContext) -> keymap::Context; + fn keymap_context(&self, cx: &AppContext) -> KeymapContext; fn debug_json(&self, cx: &AppContext) -> serde_json::Value; fn text_for_range(&self, range: Range, cx: &AppContext) -> Option; @@ -3281,7 +3282,7 @@ where View::modifiers_changed(self, event, &mut cx) } - fn keymap_context(&self, cx: &AppContext) -> keymap::Context { + fn keymap_context(&self, cx: &AppContext) -> KeymapContext { View::keymap_context(self, cx) } @@ -6633,7 +6634,7 @@ mod tests { struct View { id: usize, - keymap_context: keymap::Context, + keymap_context: KeymapContext, } impl Entity for View { @@ -6649,7 +6650,7 @@ mod tests { "View" } - fn keymap_context(&self, _: &AppContext) -> keymap::Context { + fn keymap_context(&self, _: &AppContext) -> KeymapContext { self.keymap_context.clone() } } @@ -6658,7 +6659,7 @@ mod tests { fn new(id: usize) -> Self { View { id, - keymap_context: keymap::Context::default(), + keymap_context: KeymapContext::default(), } } } @@ -6682,17 +6683,13 @@ mod tests { // This keymap's only binding dispatches an action on view 2 because that view will have // "a" and "b" in its context, but not "c". - cx.add_bindings(vec![keymap::Binding::new( + cx.add_bindings(vec![Binding::new( "a", Action("a".to_string()), Some("a && b && !c"), )]); - cx.add_bindings(vec![keymap::Binding::new( - "b", - Action("b".to_string()), - None, - )]); + cx.add_bindings(vec![Binding::new("b", Action("b".to_string()), None)]); let actions = Rc::new(RefCell::new(Vec::new())); cx.add_action({ diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 72f1f546fb..67455cd2a7 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -17,11 +17,11 @@ use parking_lot::{Mutex, RwLock}; use smol::stream::StreamExt; use crate::{ - executor, geometry::vector::Vector2F, keymap::Keystroke, platform, Action, AnyViewHandle, - AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, LeakDetector, - ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith, - RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle, - WindowInputHandler, + executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action, + AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, + LeakDetector, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, + ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, + WeakHandle, WindowInputHandler, }; use collections::BTreeMap; diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index d8b446ac17..dfdc269aff 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -25,7 +25,7 @@ pub mod executor; pub use executor::Task; pub mod color; pub mod json; -pub mod keymap; +pub mod keymap_matcher; pub mod platform; pub use gpui_macros::test; pub use platform::*; diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs deleted file mode 100644 index e9bc228757..0000000000 --- a/crates/gpui/src/keymap.rs +++ /dev/null @@ -1,757 +0,0 @@ -use crate::Action; -use anyhow::{anyhow, Result}; -use smallvec::SmallVec; -use std::{ - any::{Any, TypeId}, - collections::{HashMap, HashSet}, - fmt::{Debug, Write}, -}; -use tree_sitter::{Language, Node, Parser}; - -extern "C" { - fn tree_sitter_context_predicate() -> Language; -} - -pub struct Matcher { - pending_views: HashMap, - pending_keystrokes: Vec, - keymap: Keymap, -} - -#[derive(Default)] -pub struct Keymap { - bindings: Vec, - binding_indices_by_action_type: HashMap>, -} - -pub struct Binding { - keystrokes: SmallVec<[Keystroke; 2]>, - action: Box, - context_predicate: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Keystroke { - pub ctrl: bool, - pub alt: bool, - pub shift: bool, - pub cmd: bool, - pub function: bool, - pub key: String, -} - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct Context { - pub set: HashSet, - pub map: HashMap, -} - -#[derive(Debug, Eq, PartialEq)] -enum ContextPredicate { - Identifier(String), - Equal(String, String), - NotEqual(String, String), - Not(Box), - And(Box, Box), - Or(Box, Box), -} - -trait ActionArg { - fn boxed_clone(&self) -> Box; -} - -impl ActionArg for T -where - T: 'static + Any + Clone, -{ - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } -} - -pub enum MatchResult { - None, - Pending, - Matches(Vec<(usize, Box)>), -} - -impl Debug for MatchResult { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - MatchResult::None => f.debug_struct("MatchResult::None").finish(), - MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(), - MatchResult::Matches(matches) => f - .debug_list() - .entries( - matches - .iter() - .map(|(view_id, action)| format!("{view_id}, {}", action.name())), - ) - .finish(), - } - } -} - -impl PartialEq for MatchResult { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (MatchResult::None, MatchResult::None) => true, - (MatchResult::Pending, MatchResult::Pending) => true, - (MatchResult::Matches(matches), MatchResult::Matches(other_matches)) => { - matches.len() == other_matches.len() - && matches.iter().zip(other_matches.iter()).all( - |((view_id, action), (other_view_id, other_action))| { - view_id == other_view_id && action.eq(other_action.as_ref()) - }, - ) - } - _ => false, - } - } -} - -impl Eq for MatchResult {} - -impl Clone for MatchResult { - fn clone(&self) -> Self { - match self { - MatchResult::None => MatchResult::None, - MatchResult::Pending => MatchResult::Pending, - MatchResult::Matches(matches) => MatchResult::Matches( - matches - .iter() - .map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref()))) - .collect(), - ), - } - } -} - -impl Matcher { - pub fn new(keymap: Keymap) -> Self { - Self { - pending_views: HashMap::new(), - pending_keystrokes: Vec::new(), - keymap, - } - } - - pub fn set_keymap(&mut self, keymap: Keymap) { - self.clear_pending(); - self.keymap = keymap; - } - - pub fn add_bindings>(&mut self, bindings: T) { - self.clear_pending(); - self.keymap.add_bindings(bindings); - } - - pub fn clear_bindings(&mut self) { - self.clear_pending(); - self.keymap.clear(); - } - - pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator { - self.keymap.bindings_for_action_type(action_type) - } - - pub fn clear_pending(&mut self) { - self.pending_keystrokes.clear(); - self.pending_views.clear(); - } - - pub fn has_pending_keystrokes(&self) -> bool { - !self.pending_keystrokes.is_empty() - } - - pub fn push_keystroke( - &mut self, - keystroke: Keystroke, - dispatch_path: Vec<(usize, Context)>, - ) -> MatchResult { - let mut any_pending = false; - let mut matched_bindings = Vec::new(); - - let first_keystroke = self.pending_keystrokes.is_empty(); - self.pending_keystrokes.push(keystroke); - - for (view_id, context) in dispatch_path { - // Don't require pending view entry if there are no pending keystrokes - if !first_keystroke && !self.pending_views.contains_key(&view_id) { - continue; - } - - // If there is a previous view context, invalidate that view if it - // has changed - if let Some(previous_view_context) = self.pending_views.remove(&view_id) { - if previous_view_context != context { - continue; - } - } - - // Find the bindings which map the pending keystrokes and current context - for binding in self.keymap.bindings.iter().rev() { - if binding.keystrokes.starts_with(&self.pending_keystrokes) - && binding - .context_predicate - .as_ref() - .map(|c| c.eval(&context)) - .unwrap_or(true) - { - // If the binding is completed, push it onto the matches list - if binding.keystrokes.len() == self.pending_keystrokes.len() { - matched_bindings.push((view_id, binding.action.boxed_clone())); - } else { - // Otherwise, the binding is still pending - self.pending_views.insert(view_id, context.clone()); - any_pending = true; - } - } - } - } - - if !any_pending { - self.clear_pending(); - } - - if !matched_bindings.is_empty() { - MatchResult::Matches(matched_bindings) - } else if any_pending { - MatchResult::Pending - } else { - MatchResult::None - } - } - - pub fn keystrokes_for_action( - &self, - action: &dyn Action, - cx: &Context, - ) -> Option> { - for binding in self.keymap.bindings.iter().rev() { - if binding.action.eq(action) - && binding - .context_predicate - .as_ref() - .map_or(true, |predicate| predicate.eval(cx)) - { - return Some(binding.keystrokes.clone()); - } - } - None - } -} - -impl Default for Matcher { - fn default() -> Self { - Self::new(Keymap::default()) - } -} - -impl Keymap { - pub fn new(bindings: Vec) -> Self { - let mut binding_indices_by_action_type = HashMap::new(); - for (ix, binding) in bindings.iter().enumerate() { - binding_indices_by_action_type - .entry(binding.action.as_any().type_id()) - .or_insert_with(SmallVec::new) - .push(ix); - } - Self { - binding_indices_by_action_type, - bindings, - } - } - - fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator { - self.binding_indices_by_action_type - .get(&action_type) - .map(SmallVec::as_slice) - .unwrap_or(&[]) - .iter() - .map(|ix| &self.bindings[*ix]) - } - - fn add_bindings>(&mut self, bindings: T) { - for binding in bindings { - self.binding_indices_by_action_type - .entry(binding.action.as_any().type_id()) - .or_default() - .push(self.bindings.len()); - self.bindings.push(binding); - } - } - - fn clear(&mut self) { - self.bindings.clear(); - self.binding_indices_by_action_type.clear(); - } -} - -impl Binding { - pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { - Self::load(keystrokes, Box::new(action), context).unwrap() - } - - pub fn load(keystrokes: &str, action: Box, context: Option<&str>) -> Result { - let context = if let Some(context) = context { - Some(ContextPredicate::parse(context)?) - } else { - None - }; - - let keystrokes = keystrokes - .split_whitespace() - .map(Keystroke::parse) - .collect::>()?; - - Ok(Self { - keystrokes, - action, - context_predicate: context, - }) - } - - pub fn keystrokes(&self) -> &[Keystroke] { - &self.keystrokes - } - - pub fn action(&self) -> &dyn Action { - self.action.as_ref() - } -} - -impl Keystroke { - pub fn parse(source: &str) -> anyhow::Result { - let mut ctrl = false; - let mut alt = false; - let mut shift = false; - let mut cmd = false; - let mut function = false; - let mut key = None; - - let mut components = source.split('-').peekable(); - while let Some(component) = components.next() { - match component { - "ctrl" => ctrl = true, - "alt" => alt = true, - "shift" => shift = true, - "cmd" => cmd = true, - "fn" => function = true, - _ => { - if let Some(component) = components.peek() { - if component.is_empty() && source.ends_with('-') { - key = Some(String::from("-")); - break; - } else { - return Err(anyhow!("Invalid keystroke `{}`", source)); - } - } else { - key = Some(String::from(component)); - } - } - } - } - - let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?; - - Ok(Keystroke { - ctrl, - alt, - shift, - cmd, - function, - key, - }) - } - - pub fn modified(&self) -> bool { - self.ctrl || self.alt || self.shift || self.cmd - } -} - -impl std::fmt::Display for Keystroke { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.ctrl { - f.write_char('^')?; - } - if self.alt { - f.write_char('⎇')?; - } - if self.cmd { - f.write_char('⌘')?; - } - if self.shift { - f.write_char('⇧')?; - } - let key = match self.key.as_str() { - "backspace" => '⌫', - "up" => '↑', - "down" => '↓', - "left" => '←', - "right" => '→', - "tab" => '⇥', - "escape" => '⎋', - key => { - if key.len() == 1 { - key.chars().next().unwrap().to_ascii_uppercase() - } else { - return f.write_str(key); - } - } - }; - f.write_char(key) - } -} - -impl Context { - pub fn extend(&mut self, other: &Context) { - for v in &other.set { - self.set.insert(v.clone()); - } - for (k, v) in &other.map { - self.map.insert(k.clone(), v.clone()); - } - } -} - -impl ContextPredicate { - fn parse(source: &str) -> anyhow::Result { - let mut parser = Parser::new(); - let language = unsafe { tree_sitter_context_predicate() }; - parser.set_language(language).unwrap(); - let source = source.as_bytes(); - let tree = parser.parse(source, None).unwrap(); - Self::from_node(tree.root_node(), source) - } - - fn from_node(node: Node, source: &[u8]) -> anyhow::Result { - let parse_error = "error parsing context predicate"; - let kind = node.kind(); - - match kind { - "source" => Self::from_node(node.child(0).ok_or_else(|| anyhow!(parse_error))?, source), - "identifier" => Ok(Self::Identifier(node.utf8_text(source)?.into())), - "not" => { - let child = Self::from_node( - node.child_by_field_name("expression") - .ok_or_else(|| anyhow!(parse_error))?, - source, - )?; - Ok(Self::Not(Box::new(child))) - } - "and" | "or" => { - let left = Box::new(Self::from_node( - node.child_by_field_name("left") - .ok_or_else(|| anyhow!(parse_error))?, - source, - )?); - let right = Box::new(Self::from_node( - node.child_by_field_name("right") - .ok_or_else(|| anyhow!(parse_error))?, - source, - )?); - if kind == "and" { - Ok(Self::And(left, right)) - } else { - Ok(Self::Or(left, right)) - } - } - "equal" | "not_equal" => { - let left = node - .child_by_field_name("left") - .ok_or_else(|| anyhow!(parse_error))? - .utf8_text(source)? - .into(); - let right = node - .child_by_field_name("right") - .ok_or_else(|| anyhow!(parse_error))? - .utf8_text(source)? - .into(); - if kind == "equal" { - Ok(Self::Equal(left, right)) - } else { - Ok(Self::NotEqual(left, right)) - } - } - "parenthesized" => Self::from_node( - node.child_by_field_name("expression") - .ok_or_else(|| anyhow!(parse_error))?, - source, - ), - _ => Err(anyhow!(parse_error)), - } - } - - fn eval(&self, cx: &Context) -> bool { - match self { - Self::Identifier(name) => cx.set.contains(name.as_str()), - Self::Equal(left, right) => cx - .map - .get(left) - .map(|value| value == right) - .unwrap_or(false), - Self::NotEqual(left, right) => { - cx.map.get(left).map(|value| value != right).unwrap_or(true) - } - Self::Not(pred) => !pred.eval(cx), - Self::And(left, right) => left.eval(cx) && right.eval(cx), - Self::Or(left, right) => left.eval(cx) || right.eval(cx), - } - } -} - -#[cfg(test)] -mod tests { - use anyhow::Result; - use serde::Deserialize; - - use crate::{actions, impl_actions}; - - use super::*; - - #[test] - fn test_push_keystroke() -> Result<()> { - actions!(test, [B, AB, C, D, DA]); - - let mut ctx1 = Context::default(); - ctx1.set.insert("1".into()); - - let mut ctx2 = Context::default(); - ctx2.set.insert("2".into()); - - let dispatch_path = vec![(2, ctx2), (1, ctx1)]; - - let keymap = Keymap::new(vec![ - Binding::new("a b", AB, Some("1")), - Binding::new("b", B, Some("2")), - Binding::new("c", C, Some("2")), - Binding::new("d", D, Some("1")), - Binding::new("d", D, Some("2")), - Binding::new("d a", DA, Some("2")), - ]); - - let mut matcher = Matcher::new(keymap); - - // Binding with pending prefix always takes precedence - assert_eq!( - matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()), - MatchResult::Pending, - ); - // B alone doesn't match because a was pending, so AB is returned instead - assert_eq!( - matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()), - MatchResult::Matches(vec![(1, Box::new(AB))]), - ); - assert!(!matcher.has_pending_keystrokes()); - - // Without an a prefix, B is dispatched like expected - assert_eq!( - matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()), - MatchResult::Matches(vec![(2, Box::new(B))]), - ); - assert!(!matcher.has_pending_keystrokes()); - - // If a is prefixed, C will not be dispatched because there - // was a pending binding for it - assert_eq!( - matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()), - MatchResult::Pending, - ); - assert_eq!( - matcher.push_keystroke(Keystroke::parse("c")?, dispatch_path.clone()), - MatchResult::None, - ); - assert!(!matcher.has_pending_keystrokes()); - - // If a single keystroke matches multiple bindings in the tree - // all of them are returned so that we can fallback if the action - // handler decides to propagate the action - assert_eq!( - matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()), - MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]), - ); - // If none of the d action handlers consume the binding, a pending - // binding may then be used - assert_eq!( - matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()), - MatchResult::Matches(vec![(2, Box::new(DA))]), - ); - assert!(!matcher.has_pending_keystrokes()); - - Ok(()) - } - - #[test] - fn test_keystroke_parsing() -> Result<()> { - assert_eq!( - Keystroke::parse("ctrl-p")?, - Keystroke { - key: "p".into(), - ctrl: true, - alt: false, - shift: false, - cmd: false, - function: false, - } - ); - - assert_eq!( - Keystroke::parse("alt-shift-down")?, - Keystroke { - key: "down".into(), - ctrl: false, - alt: true, - shift: true, - cmd: false, - function: false, - } - ); - - assert_eq!( - Keystroke::parse("shift-cmd--")?, - Keystroke { - key: "-".into(), - ctrl: false, - alt: false, - shift: true, - cmd: true, - function: false, - } - ); - - Ok(()) - } - - #[test] - fn test_context_predicate_parsing() -> Result<()> { - use ContextPredicate::*; - - assert_eq!( - ContextPredicate::parse("a && (b == c || d != e)")?, - And( - Box::new(Identifier("a".into())), - Box::new(Or( - Box::new(Equal("b".into(), "c".into())), - Box::new(NotEqual("d".into(), "e".into())), - )) - ) - ); - - assert_eq!( - ContextPredicate::parse("!a")?, - Not(Box::new(Identifier("a".into())),) - ); - - Ok(()) - } - - #[test] - fn test_context_predicate_eval() -> Result<()> { - let predicate = ContextPredicate::parse("a && b || c == d")?; - - let mut context = Context::default(); - context.set.insert("a".into()); - assert!(!predicate.eval(&context)); - - context.set.insert("b".into()); - assert!(predicate.eval(&context)); - - context.set.remove("b"); - context.map.insert("c".into(), "x".into()); - assert!(!predicate.eval(&context)); - - context.map.insert("c".into(), "d".into()); - assert!(predicate.eval(&context)); - - let predicate = ContextPredicate::parse("!a")?; - assert!(predicate.eval(&Context::default())); - - Ok(()) - } - - #[test] - fn test_matcher() -> Result<()> { - #[derive(Clone, Deserialize, PartialEq, Eq, Debug)] - pub struct A(pub String); - impl_actions!(test, [A]); - actions!(test, [B, Ab]); - - #[derive(Clone, Debug, Eq, PartialEq)] - struct ActionArg { - a: &'static str, - } - - let keymap = Keymap::new(vec![ - Binding::new("a", A("x".to_string()), Some("a")), - Binding::new("b", B, Some("a")), - Binding::new("a b", Ab, Some("a || b")), - ]); - - let mut ctx_a = Context::default(); - ctx_a.set.insert("a".into()); - - let mut ctx_b = Context::default(); - ctx_b.set.insert("b".into()); - - let mut matcher = Matcher::new(keymap); - - // Basic match - assert_eq!( - matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_a.clone())]), - MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))]) - ); - matcher.clear_pending(); - - // Multi-keystroke match - assert_eq!( - matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_b.clone())]), - MatchResult::Pending - ); - assert_eq!( - matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_b.clone())]), - MatchResult::Matches(vec![(1, Box::new(Ab))]) - ); - matcher.clear_pending(); - - // Failed matches don't interfere with matching subsequent keys - assert_eq!( - matcher.push_keystroke(Keystroke::parse("x")?, vec![(1, ctx_a.clone())]), - MatchResult::None - ); - assert_eq!( - matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_a.clone())]), - MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))]) - ); - matcher.clear_pending(); - - // Pending keystrokes are cleared when the context changes - assert_eq!( - matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_b.clone())]), - MatchResult::Pending - ); - assert_eq!( - matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_a.clone())]), - MatchResult::None - ); - matcher.clear_pending(); - - let mut ctx_c = Context::default(); - ctx_c.set.insert("c".into()); - - // Pending keystrokes are maintained per-view - assert_eq!( - matcher.push_keystroke( - Keystroke::parse("a")?, - vec![(1, ctx_b.clone()), (2, ctx_c.clone())] - ), - MatchResult::Pending - ); - assert_eq!( - matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_b.clone())]), - MatchResult::Matches(vec![(1, Box::new(Ab))]) - ); - - Ok(()) - } -} diff --git a/crates/gpui/src/keymap_matcher.rs b/crates/gpui/src/keymap_matcher.rs new file mode 100644 index 0000000000..e007605cff --- /dev/null +++ b/crates/gpui/src/keymap_matcher.rs @@ -0,0 +1,459 @@ +mod binding; +mod keymap; +mod keymap_context; +mod keystroke; + +use std::{any::TypeId, fmt::Debug}; + +use collections::HashMap; +use serde::Deserialize; +use smallvec::SmallVec; + +use crate::{impl_actions, Action}; + +pub use binding::{Binding, BindingMatchResult}; +pub use keymap::Keymap; +pub use keymap_context::{KeymapContext, KeymapContextPredicate}; +pub use keystroke::Keystroke; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)] +pub struct KeyPressed { + #[serde(default)] + pub keystroke: Keystroke, +} + +impl_actions!(gpui, [KeyPressed]); + +pub struct KeymapMatcher { + pending_views: HashMap, + pending_keystrokes: Vec, + keymap: Keymap, +} + +impl KeymapMatcher { + pub fn new(keymap: Keymap) -> Self { + Self { + pending_views: Default::default(), + pending_keystrokes: Vec::new(), + keymap, + } + } + + pub fn set_keymap(&mut self, keymap: Keymap) { + self.clear_pending(); + self.keymap = keymap; + } + + pub fn add_bindings>(&mut self, bindings: T) { + self.clear_pending(); + self.keymap.add_bindings(bindings); + } + + pub fn clear_bindings(&mut self) { + self.clear_pending(); + self.keymap.clear(); + } + + pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator { + self.keymap.bindings_for_action_type(action_type) + } + + pub fn clear_pending(&mut self) { + self.pending_keystrokes.clear(); + self.pending_views.clear(); + } + + pub fn has_pending_keystrokes(&self) -> bool { + !self.pending_keystrokes.is_empty() + } + + pub fn push_keystroke( + &mut self, + keystroke: Keystroke, + dispatch_path: Vec<(usize, KeymapContext)>, + ) -> MatchResult { + let mut any_pending = false; + let mut matched_bindings: Vec<(usize, Box)> = Vec::new(); + + let first_keystroke = self.pending_keystrokes.is_empty(); + self.pending_keystrokes.push(keystroke.clone()); + + for (view_id, context) in dispatch_path { + // Don't require pending view entry if there are no pending keystrokes + if !first_keystroke && !self.pending_views.contains_key(&view_id) { + continue; + } + + // If there is a previous view context, invalidate that view if it + // has changed + if let Some(previous_view_context) = self.pending_views.remove(&view_id) { + if previous_view_context != context { + continue; + } + } + + // Find the bindings which map the pending keystrokes and current context + for binding in self.keymap.bindings().iter().rev() { + match binding.match_keys_and_context(&self.pending_keystrokes, &context) { + BindingMatchResult::Complete(mut action) => { + // Swap in keystroke for special KeyPressed action + if action.name() == "KeyPressed" && action.namespace() == "gpui" { + action = Box::new(KeyPressed { + keystroke: keystroke.clone(), + }); + } + matched_bindings.push((view_id, action)) + } + BindingMatchResult::Partial => { + self.pending_views.insert(view_id, context.clone()); + any_pending = true; + } + _ => {} + } + } + } + + if !any_pending { + self.clear_pending(); + } + + if !matched_bindings.is_empty() { + MatchResult::Matches(matched_bindings) + } else if any_pending { + MatchResult::Pending + } else { + MatchResult::None + } + } + + pub fn keystrokes_for_action( + &self, + action: &dyn Action, + context: &KeymapContext, + ) -> Option> { + self.keymap + .bindings() + .iter() + .rev() + .find_map(|binding| binding.keystrokes_for_action(action, context)) + } +} + +impl Default for KeymapMatcher { + fn default() -> Self { + Self::new(Keymap::default()) + } +} + +pub enum MatchResult { + None, + Pending, + Matches(Vec<(usize, Box)>), +} + +impl Debug for MatchResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MatchResult::None => f.debug_struct("MatchResult::None").finish(), + MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(), + MatchResult::Matches(matches) => f + .debug_list() + .entries( + matches + .iter() + .map(|(view_id, action)| format!("{view_id}, {}", action.name())), + ) + .finish(), + } + } +} + +impl PartialEq for MatchResult { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (MatchResult::None, MatchResult::None) => true, + (MatchResult::Pending, MatchResult::Pending) => true, + (MatchResult::Matches(matches), MatchResult::Matches(other_matches)) => { + matches.len() == other_matches.len() + && matches.iter().zip(other_matches.iter()).all( + |((view_id, action), (other_view_id, other_action))| { + view_id == other_view_id && action.eq(other_action.as_ref()) + }, + ) + } + _ => false, + } + } +} + +impl Eq for MatchResult {} + +impl Clone for MatchResult { + fn clone(&self) -> Self { + match self { + MatchResult::None => MatchResult::None, + MatchResult::Pending => MatchResult::Pending, + MatchResult::Matches(matches) => MatchResult::Matches( + matches + .iter() + .map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref()))) + .collect(), + ), + } + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use serde::Deserialize; + + use crate::{actions, impl_actions, keymap_matcher::KeymapContext}; + + use super::*; + + #[test] + fn test_push_keystroke() -> Result<()> { + actions!(test, [B, AB, C, D, DA]); + + let mut context1 = KeymapContext::default(); + context1.set.insert("1".into()); + + let mut context2 = KeymapContext::default(); + context2.set.insert("2".into()); + + let dispatch_path = vec![(2, context2), (1, context1)]; + + let keymap = Keymap::new(vec![ + Binding::new("a b", AB, Some("1")), + Binding::new("b", B, Some("2")), + Binding::new("c", C, Some("2")), + Binding::new("d", D, Some("1")), + Binding::new("d", D, Some("2")), + Binding::new("d a", DA, Some("2")), + ]); + + let mut matcher = KeymapMatcher::new(keymap); + + // Binding with pending prefix always takes precedence + assert_eq!( + matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()), + MatchResult::Pending, + ); + // B alone doesn't match because a was pending, so AB is returned instead + assert_eq!( + matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()), + MatchResult::Matches(vec![(1, Box::new(AB))]), + ); + assert!(!matcher.has_pending_keystrokes()); + + // Without an a prefix, B is dispatched like expected + assert_eq!( + matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()), + MatchResult::Matches(vec![(2, Box::new(B))]), + ); + assert!(!matcher.has_pending_keystrokes()); + + // If a is prefixed, C will not be dispatched because there + // was a pending binding for it + assert_eq!( + matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()), + MatchResult::Pending, + ); + assert_eq!( + matcher.push_keystroke(Keystroke::parse("c")?, dispatch_path.clone()), + MatchResult::None, + ); + assert!(!matcher.has_pending_keystrokes()); + + // If a single keystroke matches multiple bindings in the tree + // all of them are returned so that we can fallback if the action + // handler decides to propagate the action + assert_eq!( + matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()), + MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]), + ); + // If none of the d action handlers consume the binding, a pending + // binding may then be used + assert_eq!( + matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()), + MatchResult::Matches(vec![(2, Box::new(DA))]), + ); + assert!(!matcher.has_pending_keystrokes()); + + Ok(()) + } + + #[test] + fn test_keystroke_parsing() -> Result<()> { + assert_eq!( + Keystroke::parse("ctrl-p")?, + Keystroke { + key: "p".into(), + ctrl: true, + alt: false, + shift: false, + cmd: false, + function: false, + } + ); + + assert_eq!( + Keystroke::parse("alt-shift-down")?, + Keystroke { + key: "down".into(), + ctrl: false, + alt: true, + shift: true, + cmd: false, + function: false, + } + ); + + assert_eq!( + Keystroke::parse("shift-cmd--")?, + Keystroke { + key: "-".into(), + ctrl: false, + alt: false, + shift: true, + cmd: true, + function: false, + } + ); + + Ok(()) + } + + #[test] + fn test_context_predicate_parsing() -> Result<()> { + use KeymapContextPredicate::*; + + assert_eq!( + KeymapContextPredicate::parse("a && (b == c || d != e)")?, + And( + Box::new(Identifier("a".into())), + Box::new(Or( + Box::new(Equal("b".into(), "c".into())), + Box::new(NotEqual("d".into(), "e".into())), + )) + ) + ); + + assert_eq!( + KeymapContextPredicate::parse("!a")?, + Not(Box::new(Identifier("a".into())),) + ); + + Ok(()) + } + + #[test] + fn test_context_predicate_eval() -> Result<()> { + let predicate = KeymapContextPredicate::parse("a && b || c == d")?; + + let mut context = KeymapContext::default(); + context.set.insert("a".into()); + assert!(!predicate.eval(&context)); + + context.set.insert("b".into()); + assert!(predicate.eval(&context)); + + context.set.remove("b"); + context.map.insert("c".into(), "x".into()); + assert!(!predicate.eval(&context)); + + context.map.insert("c".into(), "d".into()); + assert!(predicate.eval(&context)); + + let predicate = KeymapContextPredicate::parse("!a")?; + assert!(predicate.eval(&KeymapContext::default())); + + Ok(()) + } + + #[test] + fn test_matcher() -> Result<()> { + #[derive(Clone, Deserialize, PartialEq, Eq, Debug)] + pub struct A(pub String); + impl_actions!(test, [A]); + actions!(test, [B, Ab]); + + #[derive(Clone, Debug, Eq, PartialEq)] + struct ActionArg { + a: &'static str, + } + + let keymap = Keymap::new(vec![ + Binding::new("a", A("x".to_string()), Some("a")), + Binding::new("b", B, Some("a")), + Binding::new("a b", Ab, Some("a || b")), + ]); + + let mut context_a = KeymapContext::default(); + context_a.set.insert("a".into()); + + let mut context_b = KeymapContext::default(); + context_b.set.insert("b".into()); + + let mut matcher = KeymapMatcher::new(keymap); + + // Basic match + assert_eq!( + matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]), + MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))]) + ); + matcher.clear_pending(); + + // Multi-keystroke match + assert_eq!( + matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]), + MatchResult::Pending + ); + assert_eq!( + matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]), + MatchResult::Matches(vec![(1, Box::new(Ab))]) + ); + matcher.clear_pending(); + + // Failed matches don't interfere with matching subsequent keys + assert_eq!( + matcher.push_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]), + MatchResult::None + ); + assert_eq!( + matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]), + MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))]) + ); + matcher.clear_pending(); + + // Pending keystrokes are cleared when the context changes + assert_eq!( + matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]), + MatchResult::Pending + ); + assert_eq!( + matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]), + MatchResult::None + ); + matcher.clear_pending(); + + let mut context_c = KeymapContext::default(); + context_c.set.insert("c".into()); + + // Pending keystrokes are maintained per-view + assert_eq!( + matcher.push_keystroke( + Keystroke::parse("a")?, + vec![(1, context_b.clone()), (2, context_c.clone())] + ), + MatchResult::Pending + ); + assert_eq!( + matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]), + MatchResult::Matches(vec![(1, Box::new(Ab))]) + ); + + Ok(()) + } +} diff --git a/crates/gpui/src/keymap_matcher/binding.rs b/crates/gpui/src/keymap_matcher/binding.rs new file mode 100644 index 0000000000..b16b7f1552 --- /dev/null +++ b/crates/gpui/src/keymap_matcher/binding.rs @@ -0,0 +1,104 @@ +use anyhow::Result; +use smallvec::SmallVec; + +use crate::Action; + +use super::{KeymapContext, KeymapContextPredicate, Keystroke}; + +pub struct Binding { + action: Box, + keystrokes: Option>, + context_predicate: Option, +} + +impl Binding { + pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { + Self::load(keystrokes, Box::new(action), context).unwrap() + } + + pub fn load(keystrokes: &str, action: Box, context: Option<&str>) -> Result { + let context = if let Some(context) = context { + Some(KeymapContextPredicate::parse(context)?) + } else { + None + }; + + let keystrokes = if keystrokes == "*" { + None // Catch all context + } else { + Some( + keystrokes + .split_whitespace() + .map(Keystroke::parse) + .collect::>()?, + ) + }; + + Ok(Self { + keystrokes, + action, + context_predicate: context, + }) + } + + fn match_context(&self, context: &KeymapContext) -> bool { + self.context_predicate + .as_ref() + .map(|predicate| predicate.eval(context)) + .unwrap_or(true) + } + + pub fn match_keys_and_context( + &self, + pending_keystrokes: &Vec, + context: &KeymapContext, + ) -> BindingMatchResult { + if self + .keystrokes + .as_ref() + .map(|keystrokes| keystrokes.starts_with(&pending_keystrokes)) + .unwrap_or(true) + && self.match_context(context) + { + // If the binding is completed, push it onto the matches list + if self + .keystrokes + .as_ref() + .map(|keystrokes| keystrokes.len() == pending_keystrokes.len()) + .unwrap_or(true) + { + BindingMatchResult::Complete(self.action.boxed_clone()) + } else { + BindingMatchResult::Partial + } + } else { + BindingMatchResult::Fail + } + } + + pub fn keystrokes_for_action( + &self, + action: &dyn Action, + context: &KeymapContext, + ) -> Option> { + if self.action.eq(action) && self.match_context(context) { + self.keystrokes.clone() + } else { + None + } + } + + pub fn keystrokes(&self) -> Option<&[Keystroke]> { + self.keystrokes.as_deref() + } + + pub fn action(&self) -> &dyn Action { + self.action.as_ref() + } +} + +pub enum BindingMatchResult { + Complete(Box), + Partial, + Fail, +} diff --git a/crates/gpui/src/keymap_matcher/keymap.rs b/crates/gpui/src/keymap_matcher/keymap.rs new file mode 100644 index 0000000000..2f33164690 --- /dev/null +++ b/crates/gpui/src/keymap_matcher/keymap.rs @@ -0,0 +1,61 @@ +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + collections::HashMap, +}; + +use super::Binding; + +#[derive(Default)] +pub struct Keymap { + bindings: Vec, + binding_indices_by_action_type: HashMap>, +} + +impl Keymap { + pub fn new(bindings: Vec) -> Self { + let mut binding_indices_by_action_type = HashMap::new(); + for (ix, binding) in bindings.iter().enumerate() { + binding_indices_by_action_type + .entry(binding.action().type_id()) + .or_insert_with(SmallVec::new) + .push(ix); + } + + Self { + binding_indices_by_action_type, + bindings, + } + } + + pub(crate) fn bindings_for_action_type( + &self, + action_type: TypeId, + ) -> impl Iterator { + self.binding_indices_by_action_type + .get(&action_type) + .map(SmallVec::as_slice) + .unwrap_or(&[]) + .iter() + .map(|ix| &self.bindings[*ix]) + } + + pub(crate) fn add_bindings>(&mut self, bindings: T) { + for binding in bindings { + self.binding_indices_by_action_type + .entry(binding.action().type_id()) + .or_default() + .push(self.bindings.len()); + self.bindings.push(binding); + } + } + + pub(crate) fn clear(&mut self) { + self.bindings.clear(); + self.binding_indices_by_action_type.clear(); + } + + pub fn bindings(&self) -> &Vec { + &self.bindings + } +} diff --git a/crates/gpui/src/keymap_matcher/keymap_context.rs b/crates/gpui/src/keymap_matcher/keymap_context.rs new file mode 100644 index 0000000000..ad7ce929fe --- /dev/null +++ b/crates/gpui/src/keymap_matcher/keymap_context.rs @@ -0,0 +1,123 @@ +use anyhow::anyhow; + +use collections::{HashMap, HashSet}; +use tree_sitter::{Language, Node, Parser}; + +extern "C" { + fn tree_sitter_context_predicate() -> Language; +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct KeymapContext { + pub set: HashSet, + pub map: HashMap, +} + +impl KeymapContext { + pub fn extend(&mut self, other: &Self) { + for v in &other.set { + self.set.insert(v.clone()); + } + for (k, v) in &other.map { + self.map.insert(k.clone(), v.clone()); + } + } +} + +#[derive(Debug, Eq, PartialEq)] +pub enum KeymapContextPredicate { + Identifier(String), + Equal(String, String), + NotEqual(String, String), + Not(Box), + And(Box, Box), + Or(Box, Box), +} + +impl KeymapContextPredicate { + pub fn parse(source: &str) -> anyhow::Result { + let mut parser = Parser::new(); + let language = unsafe { tree_sitter_context_predicate() }; + parser.set_language(language).unwrap(); + let source = source.as_bytes(); + let tree = parser.parse(source, None).unwrap(); + Self::from_node(tree.root_node(), source) + } + + fn from_node(node: Node, source: &[u8]) -> anyhow::Result { + let parse_error = "error parsing context predicate"; + let kind = node.kind(); + + match kind { + "source" => Self::from_node(node.child(0).ok_or_else(|| anyhow!(parse_error))?, source), + "identifier" => Ok(Self::Identifier(node.utf8_text(source)?.into())), + "not" => { + let child = Self::from_node( + node.child_by_field_name("expression") + .ok_or_else(|| anyhow!(parse_error))?, + source, + )?; + Ok(Self::Not(Box::new(child))) + } + "and" | "or" => { + let left = Box::new(Self::from_node( + node.child_by_field_name("left") + .ok_or_else(|| anyhow!(parse_error))?, + source, + )?); + let right = Box::new(Self::from_node( + node.child_by_field_name("right") + .ok_or_else(|| anyhow!(parse_error))?, + source, + )?); + if kind == "and" { + Ok(Self::And(left, right)) + } else { + Ok(Self::Or(left, right)) + } + } + "equal" | "not_equal" => { + let left = node + .child_by_field_name("left") + .ok_or_else(|| anyhow!(parse_error))? + .utf8_text(source)? + .into(); + let right = node + .child_by_field_name("right") + .ok_or_else(|| anyhow!(parse_error))? + .utf8_text(source)? + .into(); + if kind == "equal" { + Ok(Self::Equal(left, right)) + } else { + Ok(Self::NotEqual(left, right)) + } + } + "parenthesized" => Self::from_node( + node.child_by_field_name("expression") + .ok_or_else(|| anyhow!(parse_error))?, + source, + ), + _ => Err(anyhow!(parse_error)), + } + } + + pub fn eval(&self, context: &KeymapContext) -> bool { + match self { + Self::Identifier(name) => context.set.contains(name.as_str()), + Self::Equal(left, right) => context + .map + .get(left) + .map(|value| value == right) + .unwrap_or(false), + Self::NotEqual(left, right) => context + .map + .get(left) + .map(|value| value != right) + .unwrap_or(true), + Self::Not(pred) => !pred.eval(context), + Self::And(left, right) => left.eval(context) && right.eval(context), + Self::Or(left, right) => left.eval(context) || right.eval(context), + } + } +} diff --git a/crates/gpui/src/keymap_matcher/keystroke.rs b/crates/gpui/src/keymap_matcher/keystroke.rs new file mode 100644 index 0000000000..ed3c3f6914 --- /dev/null +++ b/crates/gpui/src/keymap_matcher/keystroke.rs @@ -0,0 +1,97 @@ +use std::fmt::Write; + +use anyhow::anyhow; +use serde::Deserialize; + +#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize)] +pub struct Keystroke { + pub ctrl: bool, + pub alt: bool, + pub shift: bool, + pub cmd: bool, + pub function: bool, + pub key: String, +} + +impl Keystroke { + pub fn parse(source: &str) -> anyhow::Result { + let mut ctrl = false; + let mut alt = false; + let mut shift = false; + let mut cmd = false; + let mut function = false; + let mut key = None; + + let mut components = source.split('-').peekable(); + while let Some(component) = components.next() { + match component { + "ctrl" => ctrl = true, + "alt" => alt = true, + "shift" => shift = true, + "cmd" => cmd = true, + "fn" => function = true, + _ => { + if let Some(component) = components.peek() { + if component.is_empty() && source.ends_with('-') { + key = Some(String::from("-")); + break; + } else { + return Err(anyhow!("Invalid keystroke `{}`", source)); + } + } else { + key = Some(String::from(component)); + } + } + } + } + + let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?; + + Ok(Keystroke { + ctrl, + alt, + shift, + cmd, + function, + key, + }) + } + + pub fn modified(&self) -> bool { + self.ctrl || self.alt || self.shift || self.cmd + } +} + +impl std::fmt::Display for Keystroke { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.ctrl { + f.write_char('^')?; + } + if self.alt { + f.write_char('⎇')?; + } + if self.cmd { + f.write_char('⌘')?; + } + if self.shift { + f.write_char('⇧')?; + } + let key = match self.key.as_str() { + "backspace" => '⌫', + "up" => '↑', + "down" => '↓', + "left" => '←', + "right" => '→', + "tab" => '⇥', + "escape" => '⎋', + key => { + if key.len() == 1 { + key.chars().next().unwrap().to_ascii_uppercase() + } else { + return f.write_str(key); + } + } + }; + f.write_char(key) + } +} diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 2920835c49..d027218040 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -14,7 +14,7 @@ use crate::{ rect::{RectF, RectI}, vector::Vector2F, }, - keymap, + keymap_matcher::KeymapMatcher, text_layout::{LineLayout, RunStyle}, Action, ClipboardItem, Menu, Scene, }; @@ -87,7 +87,7 @@ pub(crate) trait ForegroundPlatform { fn on_menu_command(&self, callback: Box); fn on_validate_menu_command(&self, callback: Box bool>); fn on_will_open_menu(&self, callback: Box); - fn set_menus(&self, menus: Vec, matcher: &keymap::Matcher); + fn set_menus(&self, menus: Vec, matcher: &KeymapMatcher); fn prompt_for_paths( &self, options: PathPromptOptions, diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index 862807a74d..0c08af4497 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use pathfinder_geometry::vector::vec2f; -use crate::{geometry::vector::Vector2F, keymap::Keystroke}; +use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke}; #[derive(Clone, Debug)] pub struct KeyDownEvent { diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 0464840e86..c527fe8d25 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -1,6 +1,6 @@ use crate::{ geometry::vector::vec2f, - keymap::Keystroke, + keymap_matcher::Keystroke, platform::{Event, NavigationDirection}, KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase, diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 3e2ef73634..0638689cd4 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -3,7 +3,8 @@ use super::{ FontSystem, Window, }; use crate::{ - executor, keymap, + executor, + keymap_matcher::KeymapMatcher, platform::{self, CursorStyle}, Action, AppVersion, ClipboardItem, Event, Menu, MenuItem, }; @@ -135,7 +136,7 @@ impl MacForegroundPlatform { menus: Vec, delegate: id, actions: &mut Vec>, - keystroke_matcher: &keymap::Matcher, + keystroke_matcher: &KeymapMatcher, ) -> id { let application_menu = NSMenu::new(nil).autorelease(); application_menu.setDelegate_(delegate); @@ -172,7 +173,7 @@ impl MacForegroundPlatform { item: MenuItem, delegate: id, actions: &mut Vec>, - keystroke_matcher: &keymap::Matcher, + keystroke_matcher: &KeymapMatcher, ) -> id { match item { MenuItem::Separator => NSMenuItem::separatorItem(nil), @@ -183,7 +184,7 @@ impl MacForegroundPlatform { .map(|binding| binding.keystrokes()); let item; - if let Some(keystrokes) = keystrokes { + if let Some(keystrokes) = keystrokes.flatten() { if keystrokes.len() == 1 { let keystroke = &keystrokes[0]; let mut mask = NSEventModifierFlags::empty(); @@ -317,7 +318,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform { self.0.borrow_mut().validate_menu_command = Some(callback); } - fn set_menus(&self, menus: Vec, keystroke_matcher: &keymap::Matcher) { + fn set_menus(&self, menus: Vec, keystroke_matcher: &KeymapMatcher) { unsafe { let app: id = msg_send![APP_CLASS, sharedApplication]; let mut state = self.0.borrow_mut(); diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 57c5c3711d..958ac2ebd6 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -4,7 +4,7 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - keymap::Keystroke, + keymap_matcher::Keystroke, mac::platform::NSViewLayerContentsRedrawDuringViewResize, platform::{ self, diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index 1bd92eb6e3..00cd524c1d 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -4,7 +4,8 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - keymap, Action, ClipboardItem, + keymap_matcher::KeymapMatcher, + Action, ClipboardItem, }; use anyhow::{anyhow, Result}; use collections::VecDeque; @@ -84,7 +85,7 @@ impl super::ForegroundPlatform for ForegroundPlatform { fn on_menu_command(&self, _: Box) {} fn on_validate_menu_command(&self, _: Box bool>) {} fn on_will_open_menu(&self, _: Box) {} - fn set_menus(&self, _: Vec, _: &keymap::Matcher) {} + fn set_menus(&self, _: Vec, _: &KeymapMatcher) {} fn prompt_for_paths( &self, diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index eb7554a39c..5c13f7467a 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -4,7 +4,7 @@ use crate::{ font_cache::FontCache, geometry::rect::RectF, json::{self, ToJson}, - keymap::Keystroke, + keymap_matcher::Keystroke, platform::{CursorStyle, Event}, scene::{ CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, diff --git a/crates/live_kit_client/examples/test_app.rs b/crates/live_kit_client/examples/test_app.rs index eddee785bc..a92b106d33 100644 --- a/crates/live_kit_client/examples/test_app.rs +++ b/crates/live_kit_client/examples/test_app.rs @@ -1,5 +1,5 @@ use futures::StreamExt; -use gpui::{actions, keymap::Binding, Menu, MenuItem}; +use gpui::{actions, keymap_matcher::Binding, Menu, MenuItem}; use live_kit_client::{LocalVideoTrack, RemoteVideoTrackUpdate, Room}; use live_kit_server::token::{self, VideoGrant}; use log::LevelFilter; diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index a9cf23fb3f..dd6ab78c29 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -2,7 +2,7 @@ use editor::Editor; use gpui::{ elements::*, geometry::vector::{vec2f, Vector2F}, - keymap, + keymap_matcher::KeymapContext, platform::CursorStyle, AnyViewHandle, AppContext, Axis, Entity, MouseButton, MouseState, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -124,7 +124,7 @@ impl View for Picker { .named("picker") } - fn keymap_context(&self, _: &AppContext) -> keymap::Context { + fn keymap_context(&self, _: &AppContext) -> KeymapContext { let mut cx = Self::default_keymap_context(); cx.set.insert("menu".into()); cx diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index e88f3004eb..0042695950 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -10,7 +10,8 @@ use gpui::{ MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState, }, geometry::vector::Vector2F, - impl_internal_actions, keymap, + impl_internal_actions, + keymap_matcher::KeymapContext, platform::CursorStyle, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, @@ -1301,7 +1302,7 @@ impl View for ProjectPanel { .boxed() } - fn keymap_context(&self, _: &AppContext) -> keymap::Context { + fn keymap_context(&self, _: &AppContext) -> KeymapContext { let mut cx = Self::default_keymap_context(); cx.set.insert("menu".into()); cx diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 4dcb5a6fb0..4090bcc63a 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -2,7 +2,7 @@ use crate::{parse_json_with_comments, Settings}; use anyhow::{Context, Result}; use assets::Assets; use collections::BTreeMap; -use gpui::{keymap::Binding, MutableAppContext}; +use gpui::{keymap_matcher::Binding, MutableAppContext}; use schemars::{ gen::{SchemaGenerator, SchemaSettings}, schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation}, diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index ddcd6c5898..9d19625971 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -1,6 +1,6 @@ /// The mappings defined in this file where created from reading the alacritty source use alacritty_terminal::term::TermMode; -use gpui::keymap::Keystroke; +use gpui::keymap_matcher::Keystroke; #[derive(Debug, PartialEq, Eq)] pub enum Modifiers { @@ -273,6 +273,8 @@ fn modifier_code(keystroke: &Keystroke) -> u32 { #[cfg(test)] mod test { + use gpui::keymap_matcher::Keystroke; + use super::*; #[test] diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 7cdac33cda..dd5c5fb3b0 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -50,7 +50,7 @@ use thiserror::Error; use gpui::{ geometry::vector::{vec2f, Vector2F}, - keymap::Keystroke, + keymap_matcher::Keystroke, scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp}, ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, Task, }; diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 7602a3db22..a4f90a8d72 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -14,7 +14,7 @@ use gpui::{ elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack, Text}, geometry::vector::Vector2F, impl_actions, impl_internal_actions, - keymap::Keystroke, + keymap_matcher::{KeymapContext, Keystroke}, AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; @@ -465,7 +465,7 @@ impl View for TerminalView { }); } - fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context { + fn keymap_context(&self, cx: &gpui::AppContext) -> KeymapContext { let mut context = Self::default_keymap_context(); let mode = self.terminal.read(cx).last_content.mode; diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 5aa1df6dd8..9089eebcb5 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -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 +} diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index bc65fbd09e..d88d496ee9 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -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ˇ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ˇ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; + } + } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 6bbab1ae42..c5e52a520d 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -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"], + _ => &[], + } } } diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index e2522a76aa..1850a03171 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -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}; diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 40cc414778..4d582fea6b 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -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); } }); diff --git a/crates/vim/test_data/test_capital_f_and_capital_t.json b/crates/vim/test_data/test_capital_f_and_capital_t.json new file mode 100644 index 0000000000..4c7f760021 --- /dev/null +++ b/crates/vim/test_data/test_capital_f_and_capital_t.json @@ -0,0 +1 @@ +[{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,11],"end":[1,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,7],"end":[1,7]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,7],"end":[1,7]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,7],"end":[1,7]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,7],"end":[1,7]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"}] \ No newline at end of file diff --git a/crates/vim/test_data/test_f_and_t.json b/crates/vim/test_data/test_f_and_t.json new file mode 100644 index 0000000000..35f4fd5e1d --- /dev/null +++ b/crates/vim/test_data/test_f_and_t.json @@ -0,0 +1 @@ +[{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,11],"end":[1,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,11],"end":[1,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,3],"end":[1,3]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,8],"end":[1,8]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,8],"end":[1,8]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,8],"end":[1,8]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,11],"end":[1,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,11],"end":[1,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,11],"end":[1,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,8],"end":[1,8]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,11],"end":[1,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,11],"end":[1,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,11],"end":[1,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,11],"end":[1,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,11],"end":[1,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[1,11],"end":[1,11]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"aaab b bb aaabaaa\n baaa bbb\n \nb\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"}] \ No newline at end of file diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 06e19bbf88..5794ca7f5f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -33,6 +33,7 @@ use gpui::{ actions, elements::*, impl_actions, impl_internal_actions, + keymap_matcher::KeymapContext, platform::{CursorStyle, WindowOptions}, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, @@ -2588,7 +2589,7 @@ impl View for Workspace { } } - fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context { + fn keymap_context(&self, _: &AppContext) -> KeymapContext { let mut keymap = Self::default_keymap_context(); if self.active_pane() == self.dock_pane() { keymap.set.insert("Dock".into());