From fa153a0d56d79fab18c9e8d341c688312cce90c4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 9 Nov 2023 13:23:30 -0700 Subject: [PATCH] Make command dispatching work --- Cargo.lock | 1 + .../command_palette2/src/command_palette.rs | 150 ++++++++++-------- crates/editor2/src/element.rs | 4 - crates/gpui2/src/action.rs | 26 ++- crates/gpui2/src/interactive.rs | 1 - crates/gpui2/src/keymap/binding.rs | 13 -- crates/gpui2/src/keymap/matcher.rs | 1 - crates/gpui2/src/view.rs | 6 +- crates/gpui2/src/window.rs | 62 ++++++-- crates/picker2/Cargo.toml | 1 + crates/picker2/src/picker2.rs | 41 +++-- 11 files changed, 188 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4143cf8fa7..36c1a62d7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6153,6 +6153,7 @@ dependencies = [ "serde_json", "settings2", "theme2", + "ui2", "util", ] diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 508891be9e..6816ecf3a2 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -2,13 +2,14 @@ use anyhow::anyhow; use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, Action, AnyElement, AnyWindowHandle, AppContext, BorrowWindow, Div, Element, - EventEmitter, FocusHandle, Keystroke, ParentElement, Render, Styled, View, ViewContext, - VisualContext, WeakView, + actions, div, Action, AnyElement, AnyWindowHandle, AppContext, BorrowWindow, Component, Div, + Element, EventEmitter, FocusHandle, Keystroke, ParentElement, Render, StatelessInteractive, + Styled, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use std::cmp::{self, Reverse}; -use ui::modal; +use theme::ActiveTheme; +use ui::{modal, Label}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -19,29 +20,17 @@ use zed_actions::OpenZedURL; actions!(Toggle); pub fn init(cx: &mut AppContext) { - dbg!("init"); cx.set_global(HitCounts::default()); cx.observe_new_views( |workspace: &mut Workspace, _: &mut ViewContext| { - dbg!("new workspace found"); - workspace - .modal_layer() - .register_modal(Toggle, |workspace, cx| { - dbg!("hitting cmd-shift-p"); - let Some(focus_handle) = cx.focused() else { - return None; - }; + workspace.modal_layer().register_modal(Toggle, |_, cx| { + let Some(previous_focus_handle) = cx.focused() else { + return None; + }; - let available_actions = cx.available_actions(); - dbg!(&available_actions); - - Some(cx.build_view(|cx| { - let delegate = - CommandPaletteDelegate::new(cx.view().downgrade(), focus_handle); - CommandPalette::new(delegate, cx) - })) - }); + Some(cx.build_view(|cx| CommandPalette::new(previous_focus_handle, cx))) + }); }, ) .detach(); @@ -52,8 +41,35 @@ pub struct CommandPalette { } impl CommandPalette { - fn new(delegate: CommandPaletteDelegate, cx: &mut ViewContext) -> Self { - let picker = cx.build_view(|cx| Picker::new(delegate, cx)); + fn new(previous_focus_handle: FocusHandle, cx: &mut ViewContext) -> Self { + let filter = cx.try_global::(); + + let commands = cx + .available_actions() + .into_iter() + .filter_map(|action| { + let name = action.name(); + let namespace = name.split("::").next().unwrap_or("malformed action name"); + if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) { + return None; + } + + Some(Command { + name: humanize_action_name(&name), + action, + keystrokes: vec![], // todo!() + }) + }) + .collect(); + + let delegate = + CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle, cx); + + let picker = cx.build_view(|cx| { + let picker = Picker::new(delegate, cx); + picker.focus(cx); + picker + }); Self { picker } } } @@ -78,19 +94,10 @@ pub struct CommandInterceptResult { pub struct CommandPaletteDelegate { command_palette: WeakView, - actions: Vec, + commands: Vec, matches: Vec, selected_ix: usize, - focus_handle: FocusHandle, -} - -pub enum Event { - Dismissed, - Confirmed { - window: AnyWindowHandle, - focused_view_id: usize, - action: Box, - }, + previous_focus_handle: FocusHandle, } struct Command { @@ -115,10 +122,15 @@ impl Clone for Command { struct HitCounts(HashMap); impl CommandPaletteDelegate { - pub fn new(command_palette: WeakView, focus_handle: FocusHandle) -> Self { + fn new( + command_palette: WeakView, + commands: Vec, + previous_focus_handle: FocusHandle, + cx: &ViewContext, + ) -> Self { Self { command_palette, - actions: Default::default(), + commands, matches: vec![StringMatch { candidate_id: 0, score: 0., @@ -126,7 +138,7 @@ impl CommandPaletteDelegate { string: "Foo my bar".into(), }], selected_ix: 0, - focus_handle, + previous_focus_handle, } } } @@ -151,11 +163,11 @@ impl PickerDelegate for CommandPaletteDelegate { query: String, cx: &mut ViewContext>, ) -> gpui::Task<()> { - let view_id = &self.focus_handle; + let view_id = &self.previous_focus_handle; let window = cx.window(); cx.spawn(move |picker, mut cx| async move { let mut actions = picker - .update(&mut cx, |this, _| this.delegate.actions.clone()) + .update(&mut cx, |this, _| this.delegate.commands.clone()) .expect("todo: handle picker no longer being around"); // _ = window // .available_actions(view_id, &cx) @@ -276,7 +288,7 @@ impl PickerDelegate for CommandPaletteDelegate { picker .update(&mut cx, |picker, _| { let delegate = &mut picker.delegate; - delegate.actions = actions; + delegate.commands = actions; delegate.matches = matches; if delegate.matches.is_empty() { delegate.selected_ix = 0; @@ -290,32 +302,25 @@ impl PickerDelegate for CommandPaletteDelegate { } fn dismissed(&mut self, cx: &mut ViewContext>) { - dbg!("dismissed"); self.command_palette - .update(cx, |command_palette, cx| cx.emit(ModalEvent::Dismissed)) + .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) .log_err(); } fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { - // if !self.matches.is_empty() { - // let window = cx.window(); - // let focused_view_id = self.focused_view_id; - // let action_ix = self.matches[self.selected_ix].candidate_id; - // let command = self.actions.remove(action_ix); - // cx.update_default_global(|hit_counts: &mut HitCounts, _| { - // *hit_counts.0.entry(command.name).or_default() += 1; - // }); - // let action = command.action; - - // cx.app_context() - // .spawn(move |mut cx| async move { - // window - // .dispatch_action(focused_view_id, action.as_ref(), &mut cx) - // .ok_or_else(|| anyhow!("window was closed")) - // }) - // .detach_and_log_err(cx); - // } - self.dismissed(cx) + if self.matches.is_empty() { + self.dismissed(cx); + return; + } + let action_ix = self.matches[self.selected_ix].candidate_id; + let command = self.commands.swap_remove(action_ix); + cx.update_global(|hit_counts: &mut HitCounts, _| { + *hit_counts.0.entry(command.name).or_default() += 1; + }); + let action = command.action; + cx.focus(&self.previous_focus_handle); + cx.dispatch_action(action); + self.dismissed(cx); } fn render_match( @@ -324,7 +329,26 @@ impl PickerDelegate for CommandPaletteDelegate { selected: bool, cx: &mut ViewContext>, ) -> Self::ListItem { - div().child("ooh yeah") + let colors = cx.theme().colors(); + let Some(command) = self + .matches + .get(ix) + .and_then(|m| self.commands.get(m.candidate_id)) + else { + return div(); + }; + + div() + .text_color(colors.text) + .when(selected, |s| { + s.border_l_10().border_color(colors.terminal_ansi_yellow) + }) + .hover(|style| { + style + .bg(colors.element_active) + .text_color(colors.text_accent) + }) + .child(Label::new(command.name.clone())) } // fn render_match( diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 8dbe989b1f..3e77a66936 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -4149,16 +4149,12 @@ fn build_key_listeners( build_key_listener( move |editor, key_down: &KeyDownEvent, dispatch_context, phase, cx| { if phase == DispatchPhase::Bubble { - dbg!(&dispatch_context); if let KeyMatch::Some(action) = cx.match_keystroke( &global_element_id, &key_down.keystroke, dispatch_context, ) { - dbg!("got action", &action); return Some(action); - } else { - dbg!("not action"); } } diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 3a1832e58c..170ddf942f 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -104,7 +104,17 @@ impl dyn Action { pub fn type_id(&self) -> TypeId { self.as_any().type_id() } + + pub fn name(&self) -> SharedString { + ACTION_REGISTRY + .read() + .names_by_type_id + .get(&self.type_id()) + .expect("type is not a registered action") + .clone() + } } + type ActionBuilder = fn(json: Option) -> anyhow::Result>; lazy_static! { @@ -114,7 +124,7 @@ lazy_static! { #[derive(Default)] struct ActionRegistry { builders_by_name: HashMap, - builders_by_type_id: HashMap, + names_by_type_id: HashMap, all_names: Vec, // So we can return a static slice. } @@ -123,20 +133,22 @@ pub fn register_action() { let name = A::qualified_name(); let mut lock = ACTION_REGISTRY.write(); lock.builders_by_name.insert(name.clone(), A::build); - lock.builders_by_type_id.insert(TypeId::of::(), A::build); + lock.names_by_type_id + .insert(TypeId::of::(), name.clone()); lock.all_names.push(name); } /// Construct an action based on its name and optional JSON parameters sourced from the keymap. pub fn build_action_from_type(type_id: &TypeId) -> Result> { let lock = ACTION_REGISTRY.read(); - - let build_action = lock - .builders_by_type_id + let name = lock + .names_by_type_id .get(type_id) - .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?; + .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))? + .clone(); + drop(lock); - (build_action)(None) + build_action(&name, None) } /// Construct an action based on its name and optional JSON parameters sourced from the keymap. diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 51efde62c1..a546c1b40b 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -414,7 +414,6 @@ pub trait ElementInteractivity: 'static { Box::new(move |_, key_down, context, phase, cx| { if phase == DispatchPhase::Bubble { let key_down = key_down.downcast_ref::().unwrap(); - dbg!(&context); if let KeyMatch::Some(action) = cx.match_keystroke(&global_id, &key_down.keystroke, context) { diff --git a/crates/gpui2/src/keymap/binding.rs b/crates/gpui2/src/keymap/binding.rs index 67041dc488..829f7a3b2c 100644 --- a/crates/gpui2/src/keymap/binding.rs +++ b/crates/gpui2/src/keymap/binding.rs @@ -44,19 +44,6 @@ impl KeyBinding { pending_keystrokes: &[Keystroke], contexts: &[&DispatchContext], ) -> KeyMatch { - let should_debug = self.keystrokes.len() == 1 - && self.keystrokes[0].key == "p" - && self.keystrokes[0].modifiers.command == true - && self.keystrokes[0].modifiers.shift == true; - - if false && should_debug { - dbg!( - &self.keystrokes, - &pending_keystrokes, - &contexts, - &self.matches_context(contexts) - ); - } if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.matches_context(contexts) { diff --git a/crates/gpui2/src/keymap/matcher.rs b/crates/gpui2/src/keymap/matcher.rs index c86b65c47e..c2033a9595 100644 --- a/crates/gpui2/src/keymap/matcher.rs +++ b/crates/gpui2/src/keymap/matcher.rs @@ -46,7 +46,6 @@ impl KeyMatcher { keystroke: &Keystroke, context_stack: &[&DispatchContext], ) -> KeyMatch { - dbg!(keystroke, &context_stack); let keymap = self.keymap.lock(); // Clear pending keystrokes if the keymap has changed since the last matched keystroke. if keymap.version() != self.keymap_version { diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index d12d84f43b..ffea7c4517 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -145,7 +145,7 @@ impl Eq for WeakView {} #[derive(Clone, Debug)] pub struct AnyView { model: AnyModel, - initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, + pub initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId, paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), } @@ -184,6 +184,10 @@ impl AnyView { .compute_layout(layout_id, available_space); (self.paint)(self, &mut rendered_element, cx); } + + pub(crate) fn draw_dispatch_stack(&self, cx: &mut WindowContext) { + (self.initialize)(self, cx); + } } impl Component for AnyView { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 123a516b02..6a464a4554 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -228,7 +228,7 @@ pub(crate) struct Frame { key_matchers: HashMap, mouse_listeners: HashMap>, pub(crate) focus_listeners: Vec, - key_dispatch_stack: Vec, + pub(crate) key_dispatch_stack: Vec, freeze_key_dispatch_stack: bool, focus_parents_by_child: HashMap, pub(crate) scene_builder: SceneBuilder, @@ -327,7 +327,7 @@ impl Window { /// find the focused element. We interleave key listeners with dispatch contexts so we can use the /// contexts when matching key events against the keymap. A key listener can be either an action /// handler or a [KeyDown] / [KeyUp] event listener. -enum KeyDispatchStackFrame { +pub(crate) enum KeyDispatchStackFrame { Listener { event_type: TypeId, listener: AnyKeyListener, @@ -407,6 +407,9 @@ impl<'a> WindowContext<'a> { } self.window.focus = Some(handle.id); + + // self.window.current_frame.key_dispatch_stack.clear() + // self.window.root_view.initialize() self.app.push_effect(Effect::FocusChanged { window_handle: self.window.handle, focused: Some(handle.id), @@ -428,6 +431,14 @@ impl<'a> WindowContext<'a> { self.notify(); } + pub fn dispatch_action(&mut self, action: Box) { + self.defer(|cx| { + cx.app.propagate_event = true; + let stack = cx.dispatch_stack(); + cx.dispatch_action_internal(action, &stack[..]) + }) + } + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities /// that are currently on the stack to be returned to the app. pub fn defer(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) { @@ -1055,6 +1066,26 @@ impl<'a> WindowContext<'a> { self.window.dirty = false; } + pub(crate) fn dispatch_stack(&mut self) -> Vec { + let root_view = self.window.root_view.take().unwrap(); + let window = &mut *self.window; + let mut spare_frame = Frame::default(); + mem::swap(&mut spare_frame, &mut window.previous_frame); + + self.start_frame(); + + root_view.draw_dispatch_stack(self); + + let window = &mut *self.window; + // restore the old values of current and previous frame, + // putting the new frame into spare_frame. + mem::swap(&mut window.current_frame, &mut window.previous_frame); + mem::swap(&mut spare_frame, &mut window.previous_frame); + self.window.root_view = Some(root_view); + + spare_frame.key_dispatch_stack + } + /// Rotate the current frame and the previous frame, then clear the current frame. /// We repopulate all state in the current frame during each paint. fn start_frame(&mut self) { @@ -1197,7 +1228,7 @@ impl<'a> WindowContext<'a> { DispatchPhase::Capture, self, ) { - self.dispatch_action(action, &key_dispatch_stack[..ix]); + self.dispatch_action_internal(action, &key_dispatch_stack[..ix]); } if !self.app.propagate_event { break; @@ -1224,7 +1255,10 @@ impl<'a> WindowContext<'a> { DispatchPhase::Bubble, self, ) { - self.dispatch_action(action, &key_dispatch_stack[..ix]); + self.dispatch_action_internal( + action, + &key_dispatch_stack[..ix], + ); } if !self.app.propagate_event { @@ -1296,11 +1330,9 @@ impl<'a> WindowContext<'a> { self.window.platform_window.prompt(level, msg, answers) } - pub fn available_actions(&mut self) -> Vec> { - let key_dispatch_stack = &self.window.current_frame.key_dispatch_stack; - let mut actions = Vec::new(); - dbg!(key_dispatch_stack.len()); - for frame in key_dispatch_stack { + pub fn available_actions(&self) -> impl Iterator> + '_ { + let key_dispatch_stack = &self.window.previous_frame.key_dispatch_stack; + key_dispatch_stack.iter().filter_map(|frame| { match frame { // todo!factor out a KeyDispatchStackFrame::Action KeyDispatchStackFrame::Listener { @@ -1308,21 +1340,19 @@ impl<'a> WindowContext<'a> { listener: _, } => { match build_action_from_type(event_type) { - Ok(action) => { - actions.push(action); - } + Ok(action) => Some(action), Err(err) => { dbg!(err); + None } // we'll hit his if TypeId == KeyDown } } - KeyDispatchStackFrame::Context(_) => {} + KeyDispatchStackFrame::Context(_) => None, } - } - actions + }) } - fn dispatch_action( + pub(crate) fn dispatch_action_internal( &mut self, action: Box, dispatch_stack: &[KeyDispatchStackFrame], diff --git a/crates/picker2/Cargo.toml b/crates/picker2/Cargo.toml index 90e1ae931c..3c4d21ad50 100644 --- a/crates/picker2/Cargo.toml +++ b/crates/picker2/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] editor = { package = "editor2", path = "../editor2" } +ui = { package = "ui2", path = "../ui2" } gpui = { package = "gpui2", path = "../gpui2" } menu = { package = "menu2", path = "../menu2" } settings = { package = "settings2", path = "../settings2" } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 075cf10ff6..2651d3a190 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -5,6 +5,8 @@ use gpui::{ WindowContext, }; use std::cmp; +use theme::ActiveTheme; +use ui::v_stack; pub struct Picker { pub delegate: D, @@ -133,7 +135,7 @@ impl Picker { impl Render for Picker { type Element = Div, FocusEnabled>; - fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() .context("picker") .id("picker-container") @@ -146,18 +148,33 @@ impl Render for Picker { .on_action(Self::cancel) .on_action(Self::confirm) .on_action(Self::secondary_confirm) - .child(self.editor.clone()) .child( - uniform_list("candidates", self.delegate.match_count(), { - move |this: &mut Self, visible_range, cx| { - let selected_ix = this.delegate.selected_index(); - visible_range - .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx)) - .collect() - } - }) - .track_scroll(self.scroll_handle.clone()) - .size_full(), + v_stack().gap_px().child( + v_stack() + .py_0p5() + .px_1() + .child(div().px_2().py_0p5().child(self.editor.clone())), + ), + ) + .child( + div() + .h_px() + .w_full() + .bg(cx.theme().colors().element_background), + ) + .child( + v_stack().py_0p5().px_1().grow().max_h_96().child( + uniform_list("candidates", self.delegate.match_count(), { + move |this: &mut Self, visible_range, cx| { + let selected_ix = this.delegate.selected_index(); + visible_range + .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx)) + .collect() + } + }) + .track_scroll(self.scroll_handle.clone()) + .size_full(), + ), ) } }