working f and t bindings
This commit is contained in:
parent
6a57bd2794
commit
73e7967a12
37 changed files with 1143 additions and 860 deletions
|
@ -1,6 +1,6 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"context": "Editor && VimControl",
|
"context": "Editor && VimControl && !VimWaiting",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"g": [
|
"g": [
|
||||||
"vim::PushOperator",
|
"vim::PushOperator",
|
||||||
|
@ -53,6 +53,42 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"%": "vim::Matching",
|
"%": "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",
|
"escape": "editor::Cancel",
|
||||||
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||||
"1": [
|
"1": [
|
||||||
|
@ -94,7 +130,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_mode == normal && vim_operator == none",
|
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"c": [
|
"c": [
|
||||||
"vim::PushOperator",
|
"vim::PushOperator",
|
||||||
|
@ -173,10 +209,6 @@
|
||||||
"ctrl-e": [
|
"ctrl-e": [
|
||||||
"vim::Scroll",
|
"vim::Scroll",
|
||||||
"LineDown"
|
"LineDown"
|
||||||
],
|
|
||||||
"ctrl-y": [
|
|
||||||
"vim::Scroll",
|
|
||||||
"LineUp"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -255,7 +287,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_mode == visual",
|
"context": "Editor && vim_mode == visual && !VimWaiting",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"u": "editor::Undo",
|
"u": "editor::Undo",
|
||||||
"c": "vim::VisualChange",
|
"c": "vim::VisualChange",
|
||||||
|
@ -271,5 +303,11 @@
|
||||||
"escape": "vim::NormalBefore",
|
"escape": "vim::NormalBefore",
|
||||||
"ctrl-c": "vim::NormalBefore"
|
"ctrl-c": "vim::NormalBefore"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && VimWaiting",
|
||||||
|
"bindings": {
|
||||||
|
"*": "gpui::KeyPressed"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -8,8 +8,10 @@ use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*,
|
elements::*,
|
||||||
geometry::{rect::RectF, vector::vec2f},
|
geometry::{rect::RectF, vector::vec2f},
|
||||||
impl_actions, impl_internal_actions, keymap, AppContext, CursorStyle, Entity, ModelHandle,
|
impl_actions, impl_internal_actions,
|
||||||
MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle,
|
keymap_matcher::KeymapContext,
|
||||||
|
AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
|
||||||
|
Subscription, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use menu::{Confirm, SelectNext, SelectPrev};
|
use menu::{Confirm, SelectNext, SelectPrev};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
@ -1267,7 +1269,7 @@ impl View for ContactList {
|
||||||
"ContactList"
|
"ContactList"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||||
let mut cx = Self::default_keymap_context();
|
let mut cx = Self::default_keymap_context();
|
||||||
cx.set.insert("menu".into());
|
cx.set.insert("menu".into());
|
||||||
cx
|
cx
|
||||||
|
|
|
@ -3,7 +3,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::{ChildView, Flex, Label, ParentElement},
|
elements::{ChildView, Flex, Label, ParentElement},
|
||||||
keymap::Keystroke,
|
keymap_matcher::Keystroke,
|
||||||
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, RenderContext, View,
|
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, RenderContext, View,
|
||||||
ViewContext, ViewHandle,
|
ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
|
@ -64,8 +64,10 @@ impl CommandPalette {
|
||||||
name: humanize_action_name(name),
|
name: humanize_action_name(name),
|
||||||
action,
|
action,
|
||||||
keystrokes: bindings
|
keystrokes: bindings
|
||||||
|
.iter()
|
||||||
|
.filter_map(|binding| binding.keystrokes())
|
||||||
.last()
|
.last()
|
||||||
.map_or(Vec::new(), |binding| binding.keystrokes().to_vec()),
|
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle,
|
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext,
|
||||||
Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton, MutableAppContext, RenderContext,
|
platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton,
|
||||||
SizeConstraint, Subscription, View, ViewContext,
|
MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
|
||||||
};
|
};
|
||||||
use menu::*;
|
use menu::*;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
@ -75,7 +75,7 @@ impl View for ContextMenu {
|
||||||
"ContextMenu"
|
"ContextMenu"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||||
let mut cx = Self::default_keymap_context();
|
let mut cx = Self::default_keymap_context();
|
||||||
cx.set.insert("menu".into());
|
cx.set.insert("menu".into());
|
||||||
cx
|
cx
|
||||||
|
|
|
@ -36,6 +36,7 @@ use gpui::{
|
||||||
fonts::{self, HighlightStyle, TextStyle},
|
fonts::{self, HighlightStyle, TextStyle},
|
||||||
geometry::vector::Vector2F,
|
geometry::vector::Vector2F,
|
||||||
impl_actions, impl_internal_actions,
|
impl_actions, impl_internal_actions,
|
||||||
|
keymap_matcher::KeymapContext,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
serde_json::json,
|
serde_json::json,
|
||||||
AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
|
AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
|
||||||
|
@ -464,7 +465,7 @@ pub struct Editor {
|
||||||
searchable: bool,
|
searchable: bool,
|
||||||
cursor_shape: CursorShape,
|
cursor_shape: CursorShape,
|
||||||
workspace_id: Option<WorkspaceId>,
|
workspace_id: Option<WorkspaceId>,
|
||||||
keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
|
keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
|
||||||
input_enabled: bool,
|
input_enabled: bool,
|
||||||
leader_replica_id: Option<u16>,
|
leader_replica_id: Option<u16>,
|
||||||
remote_id: Option<ViewId>,
|
remote_id: Option<ViewId>,
|
||||||
|
@ -1225,7 +1226,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: gpui::keymap::Context) {
|
pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: KeymapContext) {
|
||||||
self.keymap_context_layers
|
self.keymap_context_layers
|
||||||
.insert(TypeId::of::<Tag>(), context);
|
.insert(TypeId::of::<Tag>(), context);
|
||||||
}
|
}
|
||||||
|
@ -6245,7 +6246,7 @@ impl View for Editor {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||||
let mut context = Self::default_keymap_context();
|
let mut context = Self::default_keymap_context();
|
||||||
let mode = match self.mode {
|
let mode = match self.mode {
|
||||||
EditorMode::SingleLine => "single_line",
|
EditorMode::SingleLine => "single_line",
|
||||||
|
|
|
@ -9,7 +9,9 @@ use indoc::indoc;
|
||||||
use crate::{
|
use crate::{
|
||||||
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
|
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 language::{Buffer, BufferSnapshot};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use util::{
|
use util::{
|
||||||
|
|
|
@ -28,7 +28,6 @@ use smol::prelude::*;
|
||||||
pub use action::*;
|
pub use action::*;
|
||||||
use callback_collection::CallbackCollection;
|
use callback_collection::CallbackCollection;
|
||||||
use collections::{hash_map::Entry, HashMap, HashSet, VecDeque};
|
use collections::{hash_map::Entry, HashMap, HashSet, VecDeque};
|
||||||
use keymap::MatchResult;
|
|
||||||
use platform::Event;
|
use platform::Event;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub use test_app_context::{ContextHandle, TestAppContext};
|
pub use test_app_context::{ContextHandle, TestAppContext};
|
||||||
|
@ -37,7 +36,7 @@ use crate::{
|
||||||
elements::ElementBox,
|
elements::ElementBox,
|
||||||
executor::{self, Task},
|
executor::{self, Task},
|
||||||
geometry::rect::RectF,
|
geometry::rect::RectF,
|
||||||
keymap::{self, Binding, Keystroke},
|
keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
|
||||||
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
|
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
|
||||||
presenter::Presenter,
|
presenter::Presenter,
|
||||||
util::post_inc,
|
util::post_inc,
|
||||||
|
@ -72,11 +71,11 @@ pub trait View: Entity + Sized {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> keymap_matcher::KeymapContext {
|
||||||
Self::default_keymap_context()
|
Self::default_keymap_context()
|
||||||
}
|
}
|
||||||
fn default_keymap_context() -> keymap::Context {
|
fn default_keymap_context() -> keymap_matcher::KeymapContext {
|
||||||
let mut cx = keymap::Context::default();
|
let mut cx = keymap_matcher::KeymapContext::default();
|
||||||
cx.set.insert(Self::ui_name().into());
|
cx.set.insert(Self::ui_name().into());
|
||||||
cx
|
cx
|
||||||
}
|
}
|
||||||
|
@ -609,7 +608,7 @@ pub struct MutableAppContext {
|
||||||
capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
|
capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
|
||||||
actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
|
actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
|
||||||
global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
|
global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
|
||||||
keystroke_matcher: keymap::Matcher,
|
keystroke_matcher: KeymapMatcher,
|
||||||
next_entity_id: usize,
|
next_entity_id: usize,
|
||||||
next_window_id: usize,
|
next_window_id: usize,
|
||||||
next_subscription_id: usize,
|
next_subscription_id: usize,
|
||||||
|
@ -668,7 +667,7 @@ impl MutableAppContext {
|
||||||
capture_actions: Default::default(),
|
capture_actions: Default::default(),
|
||||||
actions: Default::default(),
|
actions: Default::default(),
|
||||||
global_actions: Default::default(),
|
global_actions: Default::default(),
|
||||||
keystroke_matcher: keymap::Matcher::default(),
|
keystroke_matcher: KeymapMatcher::default(),
|
||||||
next_entity_id: 0,
|
next_entity_id: 0,
|
||||||
next_window_id: 0,
|
next_window_id: 0,
|
||||||
next_subscription_id: 0,
|
next_subscription_id: 0,
|
||||||
|
@ -1361,8 +1360,10 @@ impl MutableAppContext {
|
||||||
.views
|
.views
|
||||||
.get(&(window_id, *view_id))
|
.get(&(window_id, *view_id))
|
||||||
.expect("view in responder chain does not exist");
|
.expect("view in responder chain does not exist");
|
||||||
let cx = view.keymap_context(self.as_ref());
|
let keymap_context = view.keymap_context(self.as_ref());
|
||||||
let keystrokes = self.keystroke_matcher.keystrokes_for_action(action, &cx);
|
let keystrokes = self
|
||||||
|
.keystroke_matcher
|
||||||
|
.keystrokes_for_action(action, &keymap_context);
|
||||||
if keystrokes.is_some() {
|
if keystrokes.is_some() {
|
||||||
return keystrokes;
|
return keystrokes;
|
||||||
}
|
}
|
||||||
|
@ -1443,7 +1444,7 @@ impl MutableAppContext {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_bindings<T: IntoIterator<Item = keymap::Binding>>(&mut self, bindings: T) {
|
pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
|
||||||
self.keystroke_matcher.add_bindings(bindings);
|
self.keystroke_matcher.add_bindings(bindings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3139,7 +3140,7 @@ pub trait AnyView {
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
view_id: usize,
|
view_id: usize,
|
||||||
) -> bool;
|
) -> 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 debug_json(&self, cx: &AppContext) -> serde_json::Value;
|
||||||
|
|
||||||
fn text_for_range(&self, range: Range<usize>, cx: &AppContext) -> Option<String>;
|
fn text_for_range(&self, range: Range<usize>, cx: &AppContext) -> Option<String>;
|
||||||
|
@ -3281,7 +3282,7 @@ where
|
||||||
View::modifiers_changed(self, event, &mut cx)
|
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)
|
View::keymap_context(self, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6633,7 +6634,7 @@ mod tests {
|
||||||
|
|
||||||
struct View {
|
struct View {
|
||||||
id: usize,
|
id: usize,
|
||||||
keymap_context: keymap::Context,
|
keymap_context: KeymapContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for View {
|
impl Entity for View {
|
||||||
|
@ -6649,7 +6650,7 @@ mod tests {
|
||||||
"View"
|
"View"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||||
self.keymap_context.clone()
|
self.keymap_context.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6658,7 +6659,7 @@ mod tests {
|
||||||
fn new(id: usize) -> Self {
|
fn new(id: usize) -> Self {
|
||||||
View {
|
View {
|
||||||
id,
|
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
|
// 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".
|
// "a" and "b" in its context, but not "c".
|
||||||
cx.add_bindings(vec![keymap::Binding::new(
|
cx.add_bindings(vec![Binding::new(
|
||||||
"a",
|
"a",
|
||||||
Action("a".to_string()),
|
Action("a".to_string()),
|
||||||
Some("a && b && !c"),
|
Some("a && b && !c"),
|
||||||
)]);
|
)]);
|
||||||
|
|
||||||
cx.add_bindings(vec![keymap::Binding::new(
|
cx.add_bindings(vec![Binding::new("b", Action("b".to_string()), None)]);
|
||||||
"b",
|
|
||||||
Action("b".to_string()),
|
|
||||||
None,
|
|
||||||
)]);
|
|
||||||
|
|
||||||
let actions = Rc::new(RefCell::new(Vec::new()));
|
let actions = Rc::new(RefCell::new(Vec::new()));
|
||||||
cx.add_action({
|
cx.add_action({
|
||||||
|
|
|
@ -17,11 +17,11 @@ use parking_lot::{Mutex, RwLock};
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
executor, geometry::vector::Vector2F, keymap::Keystroke, platform, Action, AnyViewHandle,
|
executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action,
|
||||||
AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, LeakDetector,
|
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent,
|
||||||
ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
|
LeakDetector, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith,
|
||||||
RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
|
ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle,
|
||||||
WindowInputHandler,
|
WeakHandle, WindowInputHandler,
|
||||||
};
|
};
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub mod executor;
|
||||||
pub use executor::Task;
|
pub use executor::Task;
|
||||||
pub mod color;
|
pub mod color;
|
||||||
pub mod json;
|
pub mod json;
|
||||||
pub mod keymap;
|
pub mod keymap_matcher;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub use gpui_macros::test;
|
pub use gpui_macros::test;
|
||||||
pub use platform::*;
|
pub use platform::*;
|
||||||
|
|
|
@ -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<usize, Context>,
|
|
||||||
pending_keystrokes: Vec<Keystroke>,
|
|
||||||
keymap: Keymap,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Keymap {
|
|
||||||
bindings: Vec<Binding>,
|
|
||||||
binding_indices_by_action_type: HashMap<TypeId, SmallVec<[usize; 3]>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Binding {
|
|
||||||
keystrokes: SmallVec<[Keystroke; 2]>,
|
|
||||||
action: Box<dyn Action>,
|
|
||||||
context_predicate: Option<ContextPredicate>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<String>,
|
|
||||||
pub map: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
|
||||||
enum ContextPredicate {
|
|
||||||
Identifier(String),
|
|
||||||
Equal(String, String),
|
|
||||||
NotEqual(String, String),
|
|
||||||
Not(Box<ContextPredicate>),
|
|
||||||
And(Box<ContextPredicate>, Box<ContextPredicate>),
|
|
||||||
Or(Box<ContextPredicate>, Box<ContextPredicate>),
|
|
||||||
}
|
|
||||||
|
|
||||||
trait ActionArg {
|
|
||||||
fn boxed_clone(&self) -> Box<dyn Any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> ActionArg for T
|
|
||||||
where
|
|
||||||
T: 'static + Any + Clone,
|
|
||||||
{
|
|
||||||
fn boxed_clone(&self) -> Box<dyn Any> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum MatchResult {
|
|
||||||
None,
|
|
||||||
Pending,
|
|
||||||
Matches(Vec<(usize, Box<dyn Action>)>),
|
|
||||||
}
|
|
||||||
|
|
||||||
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<T: IntoIterator<Item = Binding>>(&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<Item = &Binding> {
|
|
||||||
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<SmallVec<[Keystroke; 2]>> {
|
|
||||||
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<Binding>) -> 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<Item = &'_ Binding> {
|
|
||||||
self.binding_indices_by_action_type
|
|
||||||
.get(&action_type)
|
|
||||||
.map(SmallVec::as_slice)
|
|
||||||
.unwrap_or(&[])
|
|
||||||
.iter()
|
|
||||||
.map(|ix| &self.bindings[*ix])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_bindings<T: IntoIterator<Item = Binding>>(&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<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
|
||||||
Self::load(keystrokes, Box::new(action), context).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
|
|
||||||
let context = if let Some(context) = context {
|
|
||||||
Some(ContextPredicate::parse(context)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let keystrokes = keystrokes
|
|
||||||
.split_whitespace()
|
|
||||||
.map(Keystroke::parse)
|
|
||||||
.collect::<Result<_>>()?;
|
|
||||||
|
|
||||||
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<Self> {
|
|
||||||
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<Self> {
|
|
||||||
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<Self> {
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
459
crates/gpui/src/keymap_matcher.rs
Normal file
459
crates/gpui/src/keymap_matcher.rs
Normal file
|
@ -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<usize, KeymapContext>,
|
||||||
|
pending_keystrokes: Vec<Keystroke>,
|
||||||
|
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<T: IntoIterator<Item = Binding>>(&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<Item = &Binding> {
|
||||||
|
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<dyn Action>)> = 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<SmallVec<[Keystroke; 2]>> {
|
||||||
|
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<dyn Action>)>),
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
104
crates/gpui/src/keymap_matcher/binding.rs
Normal file
104
crates/gpui/src/keymap_matcher/binding.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::Action;
|
||||||
|
|
||||||
|
use super::{KeymapContext, KeymapContextPredicate, Keystroke};
|
||||||
|
|
||||||
|
pub struct Binding {
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
keystrokes: Option<SmallVec<[Keystroke; 2]>>,
|
||||||
|
context_predicate: Option<KeymapContextPredicate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Binding {
|
||||||
|
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||||
|
Self::load(keystrokes, Box::new(action), context).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
|
||||||
|
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::<Result<_>>()?,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
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<Keystroke>,
|
||||||
|
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<SmallVec<[Keystroke; 2]>> {
|
||||||
|
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<dyn Action>),
|
||||||
|
Partial,
|
||||||
|
Fail,
|
||||||
|
}
|
61
crates/gpui/src/keymap_matcher/keymap.rs
Normal file
61
crates/gpui/src/keymap_matcher/keymap.rs
Normal file
|
@ -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>,
|
||||||
|
binding_indices_by_action_type: HashMap<TypeId, SmallVec<[usize; 3]>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keymap {
|
||||||
|
pub fn new(bindings: Vec<Binding>) -> 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<Item = &'_ Binding> {
|
||||||
|
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<T: IntoIterator<Item = Binding>>(&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<Binding> {
|
||||||
|
&self.bindings
|
||||||
|
}
|
||||||
|
}
|
123
crates/gpui/src/keymap_matcher/keymap_context.rs
Normal file
123
crates/gpui/src/keymap_matcher/keymap_context.rs
Normal file
|
@ -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<String>,
|
||||||
|
pub map: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<KeymapContextPredicate>),
|
||||||
|
And(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
|
||||||
|
Or(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeymapContextPredicate {
|
||||||
|
pub fn parse(source: &str) -> anyhow::Result<Self> {
|
||||||
|
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<Self> {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
97
crates/gpui/src/keymap_matcher/keystroke.rs
Normal file
97
crates/gpui/src/keymap_matcher/keystroke.rs
Normal file
|
@ -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<Self> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ use crate::{
|
||||||
rect::{RectF, RectI},
|
rect::{RectF, RectI},
|
||||||
vector::Vector2F,
|
vector::Vector2F,
|
||||||
},
|
},
|
||||||
keymap,
|
keymap_matcher::KeymapMatcher,
|
||||||
text_layout::{LineLayout, RunStyle},
|
text_layout::{LineLayout, RunStyle},
|
||||||
Action, ClipboardItem, Menu, Scene,
|
Action, ClipboardItem, Menu, Scene,
|
||||||
};
|
};
|
||||||
|
@ -87,7 +87,7 @@ pub(crate) trait ForegroundPlatform {
|
||||||
fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||||
fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||||
fn on_will_open_menu(&self, callback: Box<dyn FnMut()>);
|
fn on_will_open_menu(&self, callback: Box<dyn FnMut()>);
|
||||||
fn set_menus(&self, menus: Vec<Menu>, matcher: &keymap::Matcher);
|
fn set_menus(&self, menus: Vec<Menu>, matcher: &KeymapMatcher);
|
||||||
fn prompt_for_paths(
|
fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
options: PathPromptOptions,
|
options: PathPromptOptions,
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::ops::Deref;
|
||||||
|
|
||||||
use pathfinder_geometry::vector::vec2f;
|
use pathfinder_geometry::vector::vec2f;
|
||||||
|
|
||||||
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
|
use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct KeyDownEvent {
|
pub struct KeyDownEvent {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
geometry::vector::vec2f,
|
geometry::vector::vec2f,
|
||||||
keymap::Keystroke,
|
keymap_matcher::Keystroke,
|
||||||
platform::{Event, NavigationDirection},
|
platform::{Event, NavigationDirection},
|
||||||
KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
|
KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
|
||||||
MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||||
|
|
|
@ -3,7 +3,8 @@ use super::{
|
||||||
FontSystem, Window,
|
FontSystem, Window,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
executor, keymap,
|
executor,
|
||||||
|
keymap_matcher::KeymapMatcher,
|
||||||
platform::{self, CursorStyle},
|
platform::{self, CursorStyle},
|
||||||
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
|
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
|
||||||
};
|
};
|
||||||
|
@ -135,7 +136,7 @@ impl MacForegroundPlatform {
|
||||||
menus: Vec<Menu>,
|
menus: Vec<Menu>,
|
||||||
delegate: id,
|
delegate: id,
|
||||||
actions: &mut Vec<Box<dyn Action>>,
|
actions: &mut Vec<Box<dyn Action>>,
|
||||||
keystroke_matcher: &keymap::Matcher,
|
keystroke_matcher: &KeymapMatcher,
|
||||||
) -> id {
|
) -> id {
|
||||||
let application_menu = NSMenu::new(nil).autorelease();
|
let application_menu = NSMenu::new(nil).autorelease();
|
||||||
application_menu.setDelegate_(delegate);
|
application_menu.setDelegate_(delegate);
|
||||||
|
@ -172,7 +173,7 @@ impl MacForegroundPlatform {
|
||||||
item: MenuItem,
|
item: MenuItem,
|
||||||
delegate: id,
|
delegate: id,
|
||||||
actions: &mut Vec<Box<dyn Action>>,
|
actions: &mut Vec<Box<dyn Action>>,
|
||||||
keystroke_matcher: &keymap::Matcher,
|
keystroke_matcher: &KeymapMatcher,
|
||||||
) -> id {
|
) -> id {
|
||||||
match item {
|
match item {
|
||||||
MenuItem::Separator => NSMenuItem::separatorItem(nil),
|
MenuItem::Separator => NSMenuItem::separatorItem(nil),
|
||||||
|
@ -183,7 +184,7 @@ impl MacForegroundPlatform {
|
||||||
.map(|binding| binding.keystrokes());
|
.map(|binding| binding.keystrokes());
|
||||||
|
|
||||||
let item;
|
let item;
|
||||||
if let Some(keystrokes) = keystrokes {
|
if let Some(keystrokes) = keystrokes.flatten() {
|
||||||
if keystrokes.len() == 1 {
|
if keystrokes.len() == 1 {
|
||||||
let keystroke = &keystrokes[0];
|
let keystroke = &keystrokes[0];
|
||||||
let mut mask = NSEventModifierFlags::empty();
|
let mut mask = NSEventModifierFlags::empty();
|
||||||
|
@ -317,7 +318,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
|
||||||
self.0.borrow_mut().validate_menu_command = Some(callback);
|
self.0.borrow_mut().validate_menu_command = Some(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &keymap::Matcher) {
|
fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &KeymapMatcher) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||||
let mut state = self.0.borrow_mut();
|
let mut state = self.0.borrow_mut();
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
vector::{vec2f, Vector2F},
|
vector::{vec2f, Vector2F},
|
||||||
},
|
},
|
||||||
keymap::Keystroke,
|
keymap_matcher::Keystroke,
|
||||||
mac::platform::NSViewLayerContentsRedrawDuringViewResize,
|
mac::platform::NSViewLayerContentsRedrawDuringViewResize,
|
||||||
platform::{
|
platform::{
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -4,7 +4,8 @@ use crate::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
vector::{vec2f, Vector2F},
|
vector::{vec2f, Vector2F},
|
||||||
},
|
},
|
||||||
keymap, Action, ClipboardItem,
|
keymap_matcher::KeymapMatcher,
|
||||||
|
Action, ClipboardItem,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
|
@ -84,7 +85,7 @@ impl super::ForegroundPlatform for ForegroundPlatform {
|
||||||
fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
|
fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
|
||||||
fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
|
fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
|
||||||
fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {}
|
fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {}
|
||||||
fn set_menus(&self, _: Vec<crate::Menu>, _: &keymap::Matcher) {}
|
fn set_menus(&self, _: Vec<crate::Menu>, _: &KeymapMatcher) {}
|
||||||
|
|
||||||
fn prompt_for_paths(
|
fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
font_cache::FontCache,
|
font_cache::FontCache,
|
||||||
geometry::rect::RectF,
|
geometry::rect::RectF,
|
||||||
json::{self, ToJson},
|
json::{self, ToJson},
|
||||||
keymap::Keystroke,
|
keymap_matcher::Keystroke,
|
||||||
platform::{CursorStyle, Event},
|
platform::{CursorStyle, Event},
|
||||||
scene::{
|
scene::{
|
||||||
CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
|
CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use futures::StreamExt;
|
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_client::{LocalVideoTrack, RemoteVideoTrackUpdate, Room};
|
||||||
use live_kit_server::token::{self, VideoGrant};
|
use live_kit_server::token::{self, VideoGrant};
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
|
|
|
@ -2,7 +2,7 @@ use editor::Editor;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*,
|
elements::*,
|
||||||
geometry::vector::{vec2f, Vector2F},
|
geometry::vector::{vec2f, Vector2F},
|
||||||
keymap,
|
keymap_matcher::KeymapContext,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
AnyViewHandle, AppContext, Axis, Entity, MouseButton, MouseState, MutableAppContext,
|
AnyViewHandle, AppContext, Axis, Entity, MouseButton, MouseState, MutableAppContext,
|
||||||
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
|
@ -124,7 +124,7 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||||
.named("picker")
|
.named("picker")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||||
let mut cx = Self::default_keymap_context();
|
let mut cx = Self::default_keymap_context();
|
||||||
cx.set.insert("menu".into());
|
cx.set.insert("menu".into());
|
||||||
cx
|
cx
|
||||||
|
|
|
@ -10,7 +10,8 @@ use gpui::{
|
||||||
MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
||||||
},
|
},
|
||||||
geometry::vector::Vector2F,
|
geometry::vector::Vector2F,
|
||||||
impl_internal_actions, keymap,
|
impl_internal_actions,
|
||||||
|
keymap_matcher::KeymapContext,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton,
|
AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton,
|
||||||
MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
|
MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||||
|
@ -1301,7 +1302,7 @@ impl View for ProjectPanel {
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||||
let mut cx = Self::default_keymap_context();
|
let mut cx = Self::default_keymap_context();
|
||||||
cx.set.insert("menu".into());
|
cx.set.insert("menu".into());
|
||||||
cx
|
cx
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{parse_json_with_comments, Settings};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
use gpui::{keymap::Binding, MutableAppContext};
|
use gpui::{keymap_matcher::Binding, MutableAppContext};
|
||||||
use schemars::{
|
use schemars::{
|
||||||
gen::{SchemaGenerator, SchemaSettings},
|
gen::{SchemaGenerator, SchemaSettings},
|
||||||
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
|
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/// The mappings defined in this file where created from reading the alacritty source
|
/// The mappings defined in this file where created from reading the alacritty source
|
||||||
use alacritty_terminal::term::TermMode;
|
use alacritty_terminal::term::TermMode;
|
||||||
use gpui::keymap::Keystroke;
|
use gpui::keymap_matcher::Keystroke;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Modifiers {
|
pub enum Modifiers {
|
||||||
|
@ -273,6 +273,8 @@ fn modifier_code(keystroke: &Keystroke) -> u32 {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use gpui::keymap_matcher::Keystroke;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -50,7 +50,7 @@ use thiserror::Error;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
geometry::vector::{vec2f, Vector2F},
|
geometry::vector::{vec2f, Vector2F},
|
||||||
keymap::Keystroke,
|
keymap_matcher::Keystroke,
|
||||||
scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
|
scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
|
||||||
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, Task,
|
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, Task,
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,7 @@ use gpui::{
|
||||||
elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack, Text},
|
elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack, Text},
|
||||||
geometry::vector::Vector2F,
|
geometry::vector::Vector2F,
|
||||||
impl_actions, impl_internal_actions,
|
impl_actions, impl_internal_actions,
|
||||||
keymap::Keystroke,
|
keymap_matcher::{KeymapContext, Keystroke},
|
||||||
AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
|
AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
|
||||||
View, ViewContext, ViewHandle, WeakViewHandle,
|
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 mut context = Self::default_keymap_context();
|
||||||
|
|
||||||
let mode = self.terminal.read(cx).last_content.mode;
|
let mode = self.terminal.read(cx).last_content.mode;
|
||||||
|
|
|
@ -3,7 +3,7 @@ use editor::{
|
||||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||||
movement, Bias, CharKind, DisplayPoint,
|
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 language::{Point, Selection, SelectionGoal};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
@ -32,6 +32,8 @@ pub enum Motion {
|
||||||
StartOfDocument,
|
StartOfDocument,
|
||||||
EndOfDocument,
|
EndOfDocument,
|
||||||
Matching,
|
Matching,
|
||||||
|
FindForward { before: bool, character: char },
|
||||||
|
FindBackward { after: bool, character: char },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
|
@ -107,10 +109,34 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
|
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
|
||||||
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
|
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) {
|
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));
|
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,14 +178,16 @@ impl Motion {
|
||||||
| CurrentLine
|
| CurrentLine
|
||||||
| EndOfLine
|
| EndOfLine
|
||||||
| NextWordEnd { .. }
|
| NextWordEnd { .. }
|
||||||
| Matching => true,
|
| Matching
|
||||||
|
| FindForward { .. } => true,
|
||||||
Left
|
Left
|
||||||
| Backspace
|
| Backspace
|
||||||
| Right
|
| Right
|
||||||
| StartOfLine
|
| StartOfLine
|
||||||
| NextWordStart { .. }
|
| NextWordStart { .. }
|
||||||
| PreviousWordStart { .. }
|
| PreviousWordStart { .. }
|
||||||
| FirstNonWhitespace => false,
|
| FirstNonWhitespace
|
||||||
|
| FindBackward { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,6 +224,14 @@ impl Motion {
|
||||||
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
|
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
|
||||||
EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
|
EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
|
||||||
Matching => (matching(map, point), 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))
|
(new_point != point || self.infallible()).then_some((new_point, goal))
|
||||||
|
@ -446,3 +482,50 @@ fn matching(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
point
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ use editor::{
|
||||||
};
|
};
|
||||||
use gpui::{actions, impl_actions, MutableAppContext, ViewContext};
|
use gpui::{actions, impl_actions, MutableAppContext, ViewContext};
|
||||||
use language::{AutoindentMode, Point, SelectionGoal};
|
use language::{AutoindentMode, Point, SelectionGoal};
|
||||||
|
use log::error;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
@ -101,8 +102,9 @@ pub fn normal_motion(
|
||||||
Some(Operator::Change) => change_motion(vim, motion, times, cx),
|
Some(Operator::Change) => change_motion(vim, motion, times, cx),
|
||||||
Some(Operator::Delete) => delete_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::Yank) => yank_motion(vim, motion, times, cx),
|
||||||
_ => {
|
Some(operator) => {
|
||||||
// Can't do anything for text objects or namespace operators. Ignoring
|
// 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"]);
|
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
|
||||||
cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use gpui::keymap::Context;
|
use gpui::keymap_matcher::KeymapContext;
|
||||||
use language::CursorShape;
|
use language::CursorShape;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -29,6 +29,8 @@ pub enum Operator {
|
||||||
Delete,
|
Delete,
|
||||||
Yank,
|
Yank,
|
||||||
Object { around: bool },
|
Object { around: bool },
|
||||||
|
FindForward { before: bool },
|
||||||
|
FindBackward { after: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -54,6 +56,10 @@ impl VimState {
|
||||||
|
|
||||||
pub fn vim_controlled(&self) -> bool {
|
pub fn vim_controlled(&self) -> bool {
|
||||||
!matches!(self.mode, Mode::Insert)
|
!matches!(self.mode, Mode::Insert)
|
||||||
|
|| matches!(
|
||||||
|
self.operator_stack.last(),
|
||||||
|
Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clip_at_line_end(&self) -> bool {
|
pub fn clip_at_line_end(&self) -> bool {
|
||||||
|
@ -64,8 +70,8 @@ impl VimState {
|
||||||
!matches!(self.mode, Mode::Visual { .. })
|
!matches!(self.mode, Mode::Visual { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn keymap_context_layer(&self) -> Context {
|
pub fn keymap_context_layer(&self) -> KeymapContext {
|
||||||
let mut context = Context::default();
|
let mut context = KeymapContext::default();
|
||||||
context.map.insert(
|
context.map.insert(
|
||||||
"vim_mode".to_string(),
|
"vim_mode".to_string(),
|
||||||
match self.mode {
|
match self.mode {
|
||||||
|
@ -81,34 +87,48 @@ impl VimState {
|
||||||
}
|
}
|
||||||
|
|
||||||
let active_operator = self.operator_stack.last();
|
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
|
context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operator {
|
impl Operator {
|
||||||
pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
|
pub fn id(&self) -> &'static str {
|
||||||
let operator_context = match operator {
|
match self {
|
||||||
Some(Operator::Number(_)) => "n",
|
Operator::Number(_) => "n",
|
||||||
Some(Operator::Namespace(Namespace::G)) => "g",
|
Operator::Namespace(Namespace::G) => "g",
|
||||||
Some(Operator::Namespace(Namespace::Z)) => "z",
|
Operator::Namespace(Namespace::Z) => "z",
|
||||||
Some(Operator::Object { around: false }) => "i",
|
Operator::Object { around: false } => "i",
|
||||||
Some(Operator::Object { around: true }) => "a",
|
Operator::Object { around: true } => "a",
|
||||||
Some(Operator::Change) => "c",
|
Operator::Change => "c",
|
||||||
Some(Operator::Delete) => "d",
|
Operator::Delete => "d",
|
||||||
Some(Operator::Yank) => "y",
|
Operator::Yank => "y",
|
||||||
|
Operator::FindForward { before: false } => "f",
|
||||||
|
Operator::FindForward { before: true } => "t",
|
||||||
|
Operator::FindBackward { after: false } => "F",
|
||||||
|
Operator::FindBackward { after: true } => "T",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
None => "none",
|
pub fn context_flags(&self) -> &'static [&'static str] {
|
||||||
}
|
match self {
|
||||||
.to_owned();
|
Operator::Object { .. } => &["VimObject"],
|
||||||
|
Operator::FindForward { .. } | Operator::FindBackward { .. } => &["VimWaiting"],
|
||||||
context
|
_ => &[],
|
||||||
.map
|
}
|
||||||
.insert("vim_operator".to_string(), operator_context);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use async_compat::Compat;
|
||||||
#[cfg(feature = "neovim")]
|
#[cfg(feature = "neovim")]
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
#[cfg(feature = "neovim")]
|
#[cfg(feature = "neovim")]
|
||||||
use gpui::keymap::Keystroke;
|
use gpui::keymap_matcher::Keystroke;
|
||||||
|
|
||||||
use language::{Point, Selection};
|
use language::{Point, Selection};
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ use editor::{Bias, Cancel, Editor};
|
||||||
use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
|
use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
|
||||||
use language::CursorShape;
|
use language::CursorShape;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use state::{Mode, Operator, VimState};
|
use state::{Mode, Operator, VimState};
|
||||||
use workspace::{self, Workspace};
|
use workspace::{self, Workspace};
|
||||||
|
@ -55,7 +54,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
|
||||||
// Editor Actions
|
// Editor Actions
|
||||||
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
|
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
|
// Otherwise forward cancel on to the editor
|
||||||
let vim = Vim::read(cx);
|
let vim = Vim::read(cx);
|
||||||
if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
|
if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
|
||||||
|
@ -81,17 +80,21 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any keystrokes not mapped to vim should clear the active operator
|
|
||||||
pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) {
|
pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) {
|
||||||
cx.observe_keystrokes(window_id, |_keystroke, _result, handled_by, cx| {
|
cx.observe_keystrokes(window_id, |_keystroke, _result, handled_by, cx| {
|
||||||
if let Some(handled_by) = handled_by {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
if vim.active_operator().is_some() {
|
if vim.active_operator().is_some() {
|
||||||
|
// If the keystroke is not handled by vim, we should clear the operator
|
||||||
vim.clear_operator(cx);
|
vim.clear_operator(cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
1
crates/vim/test_data/test_capital_f_and_capital_t.json
Normal file
1
crates/vim/test_data/test_capital_f_and_capital_t.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_f_and_t.json
Normal file
1
crates/vim/test_data/test_f_and_t.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -33,6 +33,7 @@ use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::*,
|
elements::*,
|
||||||
impl_actions, impl_internal_actions,
|
impl_actions, impl_internal_actions,
|
||||||
|
keymap_matcher::KeymapContext,
|
||||||
platform::{CursorStyle, WindowOptions},
|
platform::{CursorStyle, WindowOptions},
|
||||||
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||||
MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
|
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();
|
let mut keymap = Self::default_keymap_context();
|
||||||
if self.active_pane() == self.dock_pane() {
|
if self.active_pane() == self.dock_pane() {
|
||||||
keymap.set.insert("Dock".into());
|
keymap.set.insert("Dock".into());
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue