Detailed changes
@@ -6346,6 +6346,7 @@ dependencies = [
"env_logger 0.11.5",
"futures 0.3.30",
"gpui",
+ "itertools 0.13.0",
"language",
"lsp",
"project",
@@ -6357,6 +6358,7 @@ dependencies = [
"ui",
"util",
"workspace",
+ "zed_actions",
]
[[package]]
@@ -75,6 +75,18 @@ impl Keymap {
.filter(move |binding| binding.action().partial_eq(action))
}
+ /// all bindings for input returns all bindings that might match the input
+ /// (without checking context)
+ pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec<KeyBinding> {
+ self.bindings()
+ .rev()
+ .filter_map(|binding| {
+ binding.match_keystrokes(input).filter(|pending| !pending)?;
+ Some(binding.clone())
+ })
+ .collect()
+ }
+
/// bindings_for_input returns a list of bindings that match the given input,
/// and a boolean indicating whether or not more bindings might match if
/// the input was longer.
@@ -69,6 +69,11 @@ impl KeyBinding {
pub fn action(&self) -> &dyn Action {
self.action.as_ref()
}
+
+ /// Get the predicate used to match this binding
+ pub fn predicate(&self) -> Option<&KeyBindingContextPredicate> {
+ self.context_predicate.as_ref()
+ }
}
impl std::fmt::Debug for KeyBinding {
@@ -11,9 +11,12 @@ use std::fmt;
pub struct KeyContext(SmallVec<[ContextEntry; 1]>);
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
-struct ContextEntry {
- key: SharedString,
- value: Option<SharedString>,
+/// An entry in a KeyContext
+pub struct ContextEntry {
+ /// The key (or name if no value)
+ pub key: SharedString,
+ /// The value
+ pub value: Option<SharedString>,
}
impl<'a> TryFrom<&'a str> for KeyContext {
@@ -39,6 +42,17 @@ impl KeyContext {
context
}
+ /// Returns the primary context entry (usually the name of the component)
+ pub fn primary(&self) -> Option<&ContextEntry> {
+ self.0.iter().find(|p| p.value.is_none())
+ }
+
+ /// Returns everything except the primary context entry.
+ pub fn secondary(&self) -> impl Iterator<Item = &ContextEntry> {
+ let primary = self.primary();
+ self.0.iter().filter(move |&p| Some(p) != primary)
+ }
+
/// Parse a key context from a string.
/// The key context format is very simple:
/// - either a single identifier, such as `StatusBar`
@@ -178,6 +192,20 @@ pub enum KeyBindingContextPredicate {
),
}
+impl fmt::Display for KeyBindingContextPredicate {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Identifier(name) => write!(f, "{}", name),
+ Self::Equal(left, right) => write!(f, "{} == {}", left, right),
+ Self::NotEqual(left, right) => write!(f, "{} != {}", left, right),
+ Self::Not(pred) => write!(f, "!{}", pred),
+ Self::Child(parent, child) => write!(f, "{} > {}", parent, child),
+ Self::And(left, right) => write!(f, "({} && {})", left, right),
+ Self::Or(left, right) => write!(f, "({} || {})", left, right),
+ }
+ }
+}
+
impl KeyBindingContextPredicate {
/// Parse a string in the same format as the keymap's context field.
///
@@ -121,6 +121,32 @@ impl Keystroke {
})
}
+ /// Produces a representation of this key that Parse can understand.
+ pub fn unparse(&self) -> String {
+ let mut str = String::new();
+ if self.modifiers.control {
+ str.push_str("ctrl-");
+ }
+ if self.modifiers.alt {
+ str.push_str("alt-");
+ }
+ if self.modifiers.platform {
+ #[cfg(target_os = "macos")]
+ str.push_str("cmd-");
+
+ #[cfg(target_os = "linux")]
+ str.push_str("super-");
+
+ #[cfg(target_os = "windows")]
+ str.push_str("win-");
+ }
+ if self.modifiers.shift {
+ str.push_str("shift-");
+ }
+ str.push_str(&self.key);
+ str
+ }
+
/// Returns true if this keystroke left
/// the ime system in an incomplete state.
pub fn is_ime_in_progress(&self) -> bool {
@@ -3324,17 +3324,18 @@ impl<'a> WindowContext<'a> {
return;
}
- self.pending_input_changed();
self.propagate_event = true;
for binding in match_result.bindings {
self.dispatch_action_on_node(node_id, binding.action.as_ref());
if !self.propagate_event {
self.dispatch_keystroke_observers(event, Some(binding.action));
+ self.pending_input_changed();
return;
}
}
- self.finish_dispatch_key_event(event, dispatch_path)
+ self.finish_dispatch_key_event(event, dispatch_path);
+ self.pending_input_changed();
}
fn finish_dispatch_key_event(
@@ -3664,6 +3665,22 @@ impl<'a> WindowContext<'a> {
receiver
}
+ /// Returns the current context stack.
+ pub fn context_stack(&self) -> Vec<KeyContext> {
+ let dispatch_tree = &self.window.rendered_frame.dispatch_tree;
+ let node_id = self
+ .window
+ .focus
+ .and_then(|focus_id| dispatch_tree.focusable_node_id(focus_id))
+ .unwrap_or_else(|| dispatch_tree.root_node_id());
+
+ dispatch_tree
+ .dispatch_path(node_id)
+ .iter()
+ .filter_map(move |&node_id| dispatch_tree.node(node_id).context.clone())
+ .collect()
+ }
+
/// Returns all available actions for the focused element.
pub fn available_actions(&self) -> Vec<Box<dyn Action>> {
let node_id = self
@@ -3704,6 +3721,11 @@ impl<'a> WindowContext<'a> {
)
}
+ /// Returns key bindings that invoke the given action on the currently focused element.
+ pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec<KeyBinding> {
+ RefCell::borrow(&self.keymap).all_bindings_for_input(input)
+ }
+
/// Returns any bindings that would invoke the given action on the given focus handle if it were focused.
pub fn bindings_for_action_in(
&self,
@@ -19,6 +19,7 @@ copilot.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
+itertools.workspace = true
language.workspace = true
lsp.workspace = true
project.workspace = true
@@ -28,6 +29,7 @@ theme.workspace = true
tree-sitter.workspace = true
ui.workspace = true
workspace.workspace = true
+zed_actions.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
@@ -0,0 +1,280 @@
+use gpui::{
+ actions, Action, AppContext, EventEmitter, FocusHandle, FocusableView,
+ KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription,
+};
+use itertools::Itertools;
+use serde_json::json;
+use ui::{
+ div, h_flex, px, v_flex, ButtonCommon, Clickable, FluentBuilder, InteractiveElement, Label,
+ LabelCommon, LabelSize, ParentElement, SharedString, StatefulInteractiveElement, Styled,
+ ViewContext, VisualContext, WindowContext,
+};
+use ui::{Button, ButtonStyle};
+use workspace::Item;
+use workspace::Workspace;
+
+actions!(debug, [OpenKeyContextView]);
+
+pub fn init(cx: &mut AppContext) {
+ cx.observe_new_views(|workspace: &mut Workspace, _| {
+ workspace.register_action(|workspace, _: &OpenKeyContextView, cx| {
+ let key_context_view = cx.new_view(KeyContextView::new);
+ workspace.add_item_to_active_pane(Box::new(key_context_view), None, true, cx)
+ });
+ })
+ .detach();
+}
+
+struct KeyContextView {
+ pending_keystrokes: Option<Vec<Keystroke>>,
+ last_keystrokes: Option<SharedString>,
+ last_possibilities: Vec<(SharedString, SharedString, Option<bool>)>,
+ context_stack: Vec<KeyContext>,
+ focus_handle: FocusHandle,
+ _subscriptions: [Subscription; 2],
+}
+
+impl KeyContextView {
+ pub fn new(cx: &mut ViewContext<Self>) -> Self {
+ let sub1 = cx.observe_keystrokes(|this, e, cx| {
+ let mut pending = this.pending_keystrokes.take().unwrap_or_default();
+ pending.push(e.keystroke.clone());
+ let mut possibilities = cx.all_bindings_for_input(&pending);
+ possibilities.reverse();
+ this.context_stack = cx.context_stack();
+ this.last_keystrokes = Some(
+ json!(pending.iter().map(|p| p.unparse()).join(" "))
+ .to_string()
+ .into(),
+ );
+ this.last_possibilities = possibilities
+ .into_iter()
+ .map(|binding| {
+ let match_state = if let Some(predicate) = binding.predicate() {
+ if this.matches(predicate) {
+ if this.action_matches(&e.action, binding.action()) {
+ Some(true)
+ } else {
+ Some(false)
+ }
+ } else {
+ None
+ }
+ } else {
+ if this.action_matches(&e.action, binding.action()) {
+ Some(true)
+ } else {
+ Some(false)
+ }
+ };
+ let predicate = if let Some(predicate) = binding.predicate() {
+ format!("{}", predicate)
+ } else {
+ "".to_string()
+ };
+ let mut name = binding.action().name();
+ if name == "zed::NoAction" {
+ name = "(null)"
+ }
+
+ (
+ name.to_owned().into(),
+ json!(predicate).to_string().into(),
+ match_state,
+ )
+ })
+ .collect();
+ });
+ let sub2 = cx.observe_pending_input(|this, cx| {
+ this.pending_keystrokes = cx
+ .pending_input_keystrokes()
+ .map(|k| k.iter().cloned().collect());
+ if this.pending_keystrokes.is_some() {
+ this.last_keystrokes.take();
+ }
+ cx.notify();
+ });
+
+ Self {
+ context_stack: Vec::new(),
+ pending_keystrokes: None,
+ last_keystrokes: None,
+ last_possibilities: Vec::new(),
+ focus_handle: cx.focus_handle(),
+ _subscriptions: [sub1, sub2],
+ }
+ }
+}
+
+impl EventEmitter<()> for KeyContextView {}
+
+impl FocusableView for KeyContextView {
+ fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+impl KeyContextView {
+ fn set_context_stack(&mut self, stack: Vec<KeyContext>, cx: &mut ViewContext<Self>) {
+ self.context_stack = stack;
+ cx.notify()
+ }
+
+ fn matches(&self, predicate: &KeyBindingContextPredicate) -> bool {
+ let mut stack = self.context_stack.clone();
+ while !stack.is_empty() {
+ if predicate.eval(&stack) {
+ return true;
+ }
+ stack.pop();
+ }
+ false
+ }
+
+ fn action_matches(&self, a: &Option<Box<dyn Action>>, b: &dyn Action) -> bool {
+ if let Some(last_action) = a {
+ last_action.partial_eq(b)
+ } else {
+ b.name() == "zed::NoAction"
+ }
+ }
+}
+
+impl Item for KeyContextView {
+ type Event = ();
+
+ fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
+
+ fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
+ Some("Keyboard Context".into())
+ }
+
+ fn telemetry_event_text(&self) -> Option<&'static str> {
+ None
+ }
+
+ fn clone_on_split(
+ &self,
+ _workspace_id: Option<workspace::WorkspaceId>,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<gpui::View<Self>>
+ where
+ Self: Sized,
+ {
+ Some(cx.new_view(Self::new))
+ }
+}
+
+impl Render for KeyContextView {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl ui::IntoElement {
+ use itertools::Itertools;
+ v_flex()
+ .id("key-context-view")
+ .overflow_scroll()
+ .size_full()
+ .max_h_full()
+ .pt_4()
+ .pl_4()
+ .track_focus(&self.focus_handle)
+ .key_context("KeyContextView")
+ .on_mouse_up_out(
+ MouseButton::Left,
+ cx.listener(|this, _, cx| {
+ this.last_keystrokes.take();
+ this.set_context_stack(cx.context_stack(), cx);
+ }),
+ )
+ .on_mouse_up_out(
+ MouseButton::Right,
+ cx.listener(|_, _, cx| {
+ cx.defer(|this, cx| {
+ this.last_keystrokes.take();
+ this.set_context_stack(cx.context_stack(), cx);
+ });
+ }),
+ )
+ .child(Label::new("Keyboard Context").size(LabelSize::Large))
+ .child(Label::new("This view lets you determine the current context stack for creating custom key bindings in Zed. When a keyboard shortcut is triggered, it also shows all the possible contexts it could have triggered in, and which one matched."))
+ .child(
+ h_flex()
+ .mt_4()
+ .gap_4()
+ .child(
+ Button::new("default", "Open Documentation")
+ .style(ButtonStyle::Filled)
+ .on_click(|_, cx| cx.open_url("https://zed.dev/docs/key-bindings")),
+ )
+ .child(
+ Button::new("default", "View default keymap")
+ .style(ButtonStyle::Filled)
+ .key_binding(ui::KeyBinding::for_action(
+ &zed_actions::OpenDefaultKeymap,
+ cx,
+ ))
+ .on_click(|_, cx| {
+ cx.dispatch_action(workspace::SplitRight.boxed_clone());
+ cx.dispatch_action(zed_actions::OpenDefaultKeymap.boxed_clone());
+ }),
+ )
+ .child(
+ Button::new("default", "Edit your keymap")
+ .style(ButtonStyle::Filled)
+ .key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymap, cx))
+ .on_click(|_, cx| {
+ cx.dispatch_action(workspace::SplitRight.boxed_clone());
+ cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone());
+ }),
+ ),
+ )
+ .child(
+ Label::new("Current Context Stack")
+ .size(LabelSize::Large)
+ .mt_8(),
+ )
+ .children({
+ cx.context_stack().iter().enumerate().map(|(i, context)| {
+ let primary = context.primary().map(|e| e.key.clone()).unwrap_or_default();
+ let secondary = context
+ .secondary()
+ .map(|e| {
+ if let Some(value) = e.value.as_ref() {
+ format!("{}={}", e.key, value)
+ } else {
+ e.key.to_string()
+ }
+ })
+ .join(" ");
+ Label::new(format!("{} {}", primary, secondary)).ml(px(12. * (i + 1) as f32))
+ })
+ })
+ .child(Label::new("Last Keystroke").mt_4().size(LabelSize::Large))
+ .when_some(self.pending_keystrokes.as_ref(), |el, keystrokes| {
+ el.child(
+ Label::new(format!(
+ "Waiting for more input: {}",
+ keystrokes.iter().map(|k| k.unparse()).join(" ")
+ ))
+ .ml(px(12.)),
+ )
+ })
+ .when_some(self.last_keystrokes.as_ref(), |el, keystrokes| {
+ el.child(Label::new(format!("Typed: {}", keystrokes)).ml_4())
+ .children(
+ self.last_possibilities
+ .iter()
+ .map(|(name, predicate, state)| {
+ let (text, color) = match state {
+ Some(true) => ("(match)", ui::Color::Success),
+ Some(false) => ("(low precedence)", ui::Color::Hint),
+ None => ("(no match)", ui::Color::Error),
+ };
+ h_flex()
+ .gap_2()
+ .ml_8()
+ .child(div().min_w(px(200.)).child(Label::new(name.clone())))
+ .child(Label::new(predicate.clone()))
+ .child(Label::new(text).color(color))
+ }),
+ )
+ })
+ }
+}
@@ -1,3 +1,4 @@
+mod key_context_view;
mod lsp_log;
mod syntax_tree_view;
@@ -12,4 +13,5 @@ pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView};
pub fn init(cx: &mut AppContext) {
lsp_log::init(cx);
syntax_tree_view::init(cx);
+ key_context_view::init(cx);
}
@@ -68,7 +68,6 @@ actions!(
Hide,
HideOthers,
Minimize,
- OpenDefaultKeymap,
OpenDefaultSettings,
OpenProjectSettings,
OpenProjectTasks,
@@ -474,7 +473,7 @@ pub fn initialize_workspace(
.register_action(open_project_tasks_file)
.register_action(
move |workspace: &mut Workspace,
- _: &OpenDefaultKeymap,
+ _: &zed_actions::OpenDefaultKeymap,
cx: &mut ViewContext<Workspace>| {
open_bundled_file(
workspace,
@@ -18,7 +18,10 @@ pub fn app_menus() -> Vec<Menu> {
MenuItem::action("Open Settings", super::OpenSettings),
MenuItem::action("Open Key Bindings", zed_actions::OpenKeymap),
MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
- MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap),
+ MenuItem::action(
+ "Open Default Key Bindings",
+ zed_actions::OpenDefaultKeymap,
+ ),
MenuItem::action("Open Project Settings", super::OpenProjectSettings),
MenuItem::action("Select Theme...", theme_selector::Toggle::default()),
],
@@ -26,6 +26,7 @@ actions!(
zed,
[
OpenSettings,
+ OpenDefaultKeymap,
OpenAccountSettings,
OpenServerSettings,
Quit,