From 99f8466cb550b85afdc9fe1800f0e49829cc85d2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 13 Apr 2022 11:32:39 -0700 Subject: [PATCH 01/18] Add a gpui method for listing the available actions --- crates/gpui/src/app.rs | 44 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index acc84588b09eef2152e077dae4deda14a97a09c4..9724a6c0944c4d95663f9509c8ef3a482a4a0c17 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -726,7 +726,7 @@ pub struct MutableAppContext { foreground_platform: Rc, assets: Arc, cx: AppContext, - action_deserializers: HashMap<&'static str, DeserializeActionCallback>, + action_deserializers: HashMap<&'static str, (TypeId, DeserializeActionCallback)>, capture_actions: HashMap>>>, actions: HashMap>>>, global_actions: HashMap>, @@ -877,7 +877,8 @@ impl MutableAppContext { let callback = self .action_deserializers .get(name) - .ok_or_else(|| anyhow!("unknown action {}", name))?; + .ok_or_else(|| anyhow!("unknown action {}", name))? + .1; callback(argument.unwrap_or("{}")) .with_context(|| format!("invalid data for action {}", name)) } @@ -926,7 +927,7 @@ impl MutableAppContext { self.action_deserializers .entry(A::qualified_name()) - .or_insert(A::from_json_str); + .or_insert((TypeId::of::(), A::from_json_str)); let actions = if capture { &mut self.capture_actions @@ -965,7 +966,7 @@ impl MutableAppContext { self.action_deserializers .entry(A::qualified_name()) - .or_insert(A::from_json_str); + .or_insert((TypeId::of::(), A::from_json_str)); if self .global_actions @@ -1304,6 +1305,41 @@ impl MutableAppContext { } } + pub fn available_actions( + &self, + window_id: usize, + view_id: usize, + ) -> Vec<(&'static str, Box)> { + let mut action_types: HashSet<_> = self.global_actions.keys().copied().collect(); + + let presenter = self + .presenters_and_platform_windows + .get(&window_id) + .unwrap() + .0 + .clone(); + let dispatch_path = presenter.borrow().dispatch_path_from(view_id); + for view_id in dispatch_path { + if let Some(view) = self.views.get(&(window_id, view_id)) { + let view_type = view.as_any().type_id(); + if let Some(actions) = self.actions.get(&view_type) { + action_types.extend(actions.keys().copied()); + } + } + } + + self.action_deserializers + .iter() + .filter_map(|(name, (type_id, deserialize))| { + if action_types.contains(type_id) { + Some((*name, deserialize("{}").ok()?)) + } else { + None + } + }) + .collect() + } + pub fn dispatch_action( &mut self, window_id: usize, From 4630071f58f4d892ae3b49c74d09a6cf647349c5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 13 Apr 2022 13:20:59 -0700 Subject: [PATCH 02/18] Start work on a command palette --- Cargo.lock | 17 ++ assets/keymaps/default.json | 4 +- crates/command_palette/Cargo.toml | 24 ++ crates/command_palette/src/command_palette.rs | 179 ++++++++++++++ crates/command_palette/src/selector.rs | 222 ++++++++++++++++++ crates/file_finder/src/file_finder.rs | 23 +- crates/gpui/src/app.rs | 11 + crates/gpui/src/presenter.rs | 20 +- crates/workspace/src/menu.rs | 9 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + 11 files changed, 486 insertions(+), 25 deletions(-) create mode 100644 crates/command_palette/Cargo.toml create mode 100644 crates/command_palette/src/command_palette.rs create mode 100644 crates/command_palette/src/selector.rs diff --git a/Cargo.lock b/Cargo.lock index 4cba8251cf40f80dc0940ae20848f7a7326c881d..484dab47d84372d794780e4a8811a18b0da16edb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1143,6 +1143,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "command_palette" +version = "0.1.0" +dependencies = [ + "ctor", + "editor", + "env_logger 0.8.3", + "fuzzy", + "gpui", + "serde_json", + "settings", + "theme", + "util", + "workspace", +] + [[package]] name = "comrak" version = "0.10.1" @@ -6138,6 +6154,7 @@ dependencies = [ "client", "clock", "collections", + "command_palette", "contacts_panel", "crossbeam-channel", "ctor", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index d9d5742b663ca80a52d88f906e9ba791c9dda707..e517f853d5c66f70c2c811885c08e091bc9f946b 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -16,7 +16,8 @@ "ctrl-n": "menu::SelectNext", "cmd-up": "menu::SelectFirst", "cmd-down": "menu::SelectLast", - "enter": "menu::Confirm" + "enter": "menu::Confirm", + "escape": "menu::Cancel" }, "Pane": { "shift-cmd-{": "pane::ActivatePrevItem", @@ -52,6 +53,7 @@ "cmd-k t": "theme_selector::Reload", "cmd-t": "project_symbols::Toggle", "cmd-p": "file_finder::Toggle", + "cmd-shift-P": "command_palette::Toggle", "alt-shift-D": "diagnostics::Deploy", "ctrl-alt-cmd-j": "journal::NewJournalEntry", "cmd-1": [ diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..b360a041254a61956bfb5cb2ea92d22e1ec73ba0 --- /dev/null +++ b/crates/command_palette/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "command_palette" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/command_palette.rs" +doctest = false + +[dependencies] +editor = { path = "../editor" } +fuzzy = { path = "../fuzzy" } +gpui = { path = "../gpui" } +settings = { path = "../settings" } +util = { path = "../util" } +theme = { path = "../theme" } +workspace = { path = "../workspace" } + +[dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } +serde_json = { version = "1.0.64", features = ["preserve_order"] } +workspace = { path = "../workspace", features = ["test-support"] } +ctor = "0.1" +env_logger = "0.8" diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs new file mode 100644 index 0000000000000000000000000000000000000000..7e1c2b43b1be72b2d5c9780ab9e599138c0ae501 --- /dev/null +++ b/crates/command_palette/src/command_palette.rs @@ -0,0 +1,179 @@ +use std::cmp; + +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + actions, + elements::{ChildView, Label}, + Action, Element, Entity, MutableAppContext, View, ViewContext, ViewHandle, +}; +use selector::{SelectorModal, SelectorModalDelegate}; +use settings::Settings; +use workspace::Workspace; + +mod selector; + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(CommandPalette::toggle); + selector::init::(cx); +} + +actions!(command_palette, [Toggle]); + +pub struct CommandPalette { + selector: ViewHandle>, + actions: Vec<(&'static str, Box)>, + matches: Vec, + selected_ix: usize, + focused_view_id: usize, +} + +pub enum Event { + Dismissed, +} + +impl CommandPalette { + pub fn new( + focused_view_id: usize, + actions: Vec<(&'static str, Box)>, + cx: &mut ViewContext, + ) -> Self { + let this = cx.weak_handle(); + let selector = cx.add_view(|cx| SelectorModal::new(this, cx)); + Self { + selector, + actions, + matches: vec![], + selected_ix: 0, + focused_view_id, + } + } + + fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { + let workspace = cx.handle(); + let window_id = cx.window_id(); + let focused_view_id = cx.focused_view_id(window_id).unwrap_or(workspace.id()); + + cx.as_mut().defer(move |cx| { + let actions = cx.available_actions(window_id, focused_view_id); + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, |cx, _| { + let selector = cx.add_view(|cx| Self::new(focused_view_id, actions, cx)); + cx.subscribe(&selector, Self::on_event).detach(); + selector + }); + }); + }); + } + + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => { + workspace.dismiss_modal(cx); + } + } + } +} + +impl Entity for CommandPalette { + type Event = Event; +} + +impl View for CommandPalette { + fn ui_name() -> &'static str { + "CommandPalette" + } + + fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + ChildView::new(self.selector.clone()).boxed() + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.selector); + } +} + +impl SelectorModalDelegate for CommandPalette { + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_ix + } + + fn set_selected_index(&mut self, ix: usize) { + self.selected_ix = ix; + } + + fn update_matches( + &mut self, + query: String, + cx: &mut gpui::ViewContext, + ) -> gpui::Task<()> { + let candidates = self + .actions + .iter() + .enumerate() + .map(|(ix, (name, _))| StringMatchCandidate { + id: ix, + string: name.to_string(), + char_bag: name.chars().collect(), + }) + .collect::>(); + cx.spawn(move |this, mut cx| async move { + let matches = fuzzy::match_strings( + &candidates, + &query, + true, + 10000, + &Default::default(), + cx.background(), + ) + .await; + this.update(&mut cx, |this, _| { + this.matches = matches; + if this.matches.is_empty() { + this.selected_ix = 0; + } else { + this.selected_ix = cmp::min(this.selected_ix, this.matches.len() - 1); + } + }); + }) + } + + fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Dismissed); + } + + fn confirm(&mut self, cx: &mut ViewContext) { + if !self.matches.is_empty() { + let window_id = cx.window_id(); + let action_ix = self.matches[self.selected_ix].candidate_id; + cx.dispatch_action_at( + window_id, + self.focused_view_id, + self.actions[action_ix].1.as_ref(), + ) + } + cx.emit(Event::Dismissed); + } + + fn render_match(&self, ix: usize, selected: bool, cx: &gpui::AppContext) -> gpui::ElementBox { + let settings = cx.global::(); + let theme = &settings.theme.selector; + let style = if selected { + &theme.active_item + } else { + &theme.item + }; + Label::new(self.matches[ix].string.clone(), style.label.clone()) + .contained() + .with_style(style.container) + .boxed() + } +} diff --git a/crates/command_palette/src/selector.rs b/crates/command_palette/src/selector.rs new file mode 100644 index 0000000000000000000000000000000000000000..daa0f5ff6a1a286d4304ef56272abf3ee9d908ed --- /dev/null +++ b/crates/command_palette/src/selector.rs @@ -0,0 +1,222 @@ +use editor::Editor; +use gpui::{ + elements::{ + ChildView, Flex, FlexItem, Label, ParentElement, ScrollTarget, UniformList, + UniformListState, + }, + keymap, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task, + View, ViewContext, ViewHandle, WeakViewHandle, +}; +use settings::Settings; +use std::cmp; +use workspace::menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(SelectorModal::::select_first); + cx.add_action(SelectorModal::::select_last); + cx.add_action(SelectorModal::::select_next); + cx.add_action(SelectorModal::::select_prev); + cx.add_action(SelectorModal::::confirm); + cx.add_action(SelectorModal::::cancel); +} + +pub struct SelectorModal { + delegate: WeakViewHandle, + query_editor: ViewHandle, + list_state: UniformListState, +} + +pub trait SelectorModalDelegate: View { + fn match_count(&self) -> usize; + fn selected_index(&self) -> usize; + fn set_selected_index(&mut self, ix: usize); + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> Task<()>; + fn confirm(&mut self, cx: &mut ViewContext); + fn dismiss(&mut self, cx: &mut ViewContext); + fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox; +} + +impl Entity for SelectorModal { + type Event = (); +} + +impl View for SelectorModal { + fn ui_name() -> &'static str { + "SelectorModal" + } + + fn render(&mut self, cx: &mut RenderContext) -> gpui::ElementBox { + let settings = cx.global::(); + Flex::new(Axis::Vertical) + .with_child( + ChildView::new(&self.query_editor) + .contained() + .with_style(settings.theme.selector.input_editor.container) + .boxed(), + ) + .with_child( + FlexItem::new(self.render_matches(cx)) + .flex(1., false) + .boxed(), + ) + .contained() + .with_style(settings.theme.selector.container) + .constrained() + .with_max_width(500.0) + .with_max_height(420.0) + .aligned() + .top() + .named("selector") + } + + fn keymap_context(&self, _: &AppContext) -> keymap::Context { + let mut cx = Self::default_keymap_context(); + cx.set.insert("menu".into()); + cx + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.query_editor); + } +} + +impl SelectorModal { + pub fn new(delegate: WeakViewHandle, cx: &mut ViewContext) -> Self { + let query_editor = cx.add_view(|cx| { + Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx) + }); + cx.subscribe(&query_editor, Self::on_query_editor_event) + .detach(); + + Self { + delegate, + query_editor, + list_state: Default::default(), + } + } + + fn render_matches(&self, cx: &AppContext) -> ElementBox { + let delegate = self.delegate.clone(); + let match_count = if let Some(delegate) = delegate.upgrade(cx) { + delegate.read(cx).match_count() + } else { + 0 + }; + + if match_count == 0 { + let settings = cx.global::(); + return Label::new( + "No matches".into(), + settings.theme.selector.empty.label.clone(), + ) + .contained() + .with_style(settings.theme.selector.empty.container) + .named("empty matches"); + } + + UniformList::new( + self.list_state.clone(), + match_count, + move |mut range, items, cx| { + let cx = cx.as_ref(); + let delegate = delegate.upgrade(cx).unwrap(); + let delegate = delegate.read(cx); + let selected_ix = delegate.selected_index(); + range.end = cmp::min(range.end, delegate.match_count()); + items.extend(range.map(move |ix| delegate.render_match(ix, ix == selected_ix, cx))); + }, + ) + .contained() + .with_margin_top(6.0) + .named("matches") + } + + fn on_query_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + if let Some(delegate) = self.delegate.upgrade(cx) { + match event { + editor::Event::BufferEdited { .. } => { + let query = self.query_editor.read(cx).text(cx); + let update = delegate.update(cx, |d, cx| d.update_matches(query, cx)); + cx.spawn(|this, mut cx| async move { + update.await; + this.update(&mut cx, |_, cx| cx.notify()); + }) + .detach(); + } + editor::Event::Blurred => delegate.update(cx, |delegate, cx| { + delegate.dismiss(cx); + }), + _ => {} + } + } + } + + fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { + if let Some(delegate) = self.delegate.upgrade(cx) { + let index = 0; + delegate.update(cx, |delegate, _| delegate.set_selected_index(0)); + self.list_state.scroll_to(ScrollTarget::Show(index)); + cx.notify(); + } + } + + fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { + if let Some(delegate) = self.delegate.upgrade(cx) { + let index = delegate.update(cx, |delegate, _| { + let match_count = delegate.match_count(); + let index = if match_count > 0 { match_count - 1 } else { 0 }; + delegate.set_selected_index(index); + index + }); + self.list_state.scroll_to(ScrollTarget::Show(index)); + cx.notify(); + } + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if let Some(delegate) = self.delegate.upgrade(cx) { + let index = delegate.update(cx, |delegate, _| { + let mut selected_index = delegate.selected_index(); + if selected_index + 1 < delegate.match_count() { + selected_index += 1; + delegate.set_selected_index(selected_index); + } + selected_index + }); + self.list_state.scroll_to(ScrollTarget::Show(index)); + cx.notify(); + } + } + + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + if let Some(delegate) = self.delegate.upgrade(cx) { + let index = delegate.update(cx, |delegate, _| { + let mut selected_index = delegate.selected_index(); + if selected_index > 0 { + selected_index -= 1; + delegate.set_selected_index(selected_index); + } + selected_index + }); + self.list_state.scroll_to(ScrollTarget::Show(index)); + cx.notify(); + } + } + + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + if let Some(delegate) = self.delegate.upgrade(cx) { + delegate.update(cx, |delegate, cx| delegate.confirm(cx)); + } + } + + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if let Some(delegate) = self.delegate.upgrade(cx) { + delegate.update(cx, |delegate, cx| delegate.dismiss(cx)); + } + } +} diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index f3f2c31d2cfd5268135570251ed1d0697ca3610c..82368788940d92b24a88bfee73b10e1380bccf74 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -275,9 +275,7 @@ impl FileFinder { fn project_updated(&mut self, _: ModelHandle, cx: &mut ViewContext) { let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); - if let Some(task) = self.spawn_search(query, cx) { - task.detach(); - } + self.spawn_search(query, cx).detach(); } fn on_query_editor_event( @@ -294,9 +292,7 @@ impl FileFinder { self.matches.clear(); cx.notify(); } else { - if let Some(task) = self.spawn_search(query, cx) { - task.detach(); - } + self.spawn_search(query, cx).detach(); } } editor::Event::Blurred => cx.emit(Event::Dismissed), @@ -354,14 +350,13 @@ impl FileFinder { cx.emit(Event::Selected(project_path.clone())); } - #[must_use] - fn spawn_search(&mut self, query: String, cx: &mut ViewContext) -> Option> { + fn spawn_search(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { let search_id = util::post_inc(&mut self.search_count); self.cancel_flag.store(true, atomic::Ordering::Relaxed); self.cancel_flag = Arc::new(AtomicBool::new(false)); let cancel_flag = self.cancel_flag.clone(); let project = self.project.clone(); - Some(cx.spawn(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { let matches = project .read_with(&cx, |project, cx| { project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx) @@ -371,7 +366,7 @@ impl FileFinder { this.update(&mut cx, |this, cx| { this.update_matches((search_id, did_cancel, query, matches), cx) }); - })) + }) } fn update_matches( @@ -514,7 +509,6 @@ mod tests { let query = "hi".to_string(); finder .update(cx, |f, cx| f.spawn_search(query.clone(), cx)) - .unwrap() .await; finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 5)); @@ -523,7 +517,7 @@ mod tests { // Simulate a search being cancelled after the time limit, // returning only a subset of the matches that would have been found. - finder.spawn_search(query.clone(), cx).unwrap().detach(); + finder.spawn_search(query.clone(), cx).detach(); finder.update_matches( ( finder.latest_search_id, @@ -535,7 +529,7 @@ mod tests { ); // Simulate another cancellation. - finder.spawn_search(query.clone(), cx).unwrap().detach(); + finder.spawn_search(query.clone(), cx).detach(); finder.update_matches( ( finder.latest_search_id, @@ -576,7 +570,6 @@ mod tests { // is included in the matching, because the worktree is a single file. finder .update(cx, |f, cx| f.spawn_search("thf".into(), cx)) - .unwrap() .await; cx.read(|cx| { let finder = finder.read(cx); @@ -594,7 +587,6 @@ mod tests { // not match anything. finder .update(cx, |f, cx| f.spawn_search("thf/".into(), cx)) - .unwrap() .await; finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 0)); } @@ -633,7 +625,6 @@ mod tests { // Run a search that matches two files with the same relative path. finder .update(cx, |f, cx| f.spawn_search("a.t".into(), cx)) - .unwrap() .await; // Can switch between different matches with the same relative path. diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 9724a6c0944c4d95663f9509c8ef3a482a4a0c17..f8721770820ce73f44fa771c200578e7df30a5fc 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1340,6 +1340,17 @@ impl MutableAppContext { .collect() } + pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: &dyn Action) { + let presenter = self + .presenters_and_platform_windows + .get(&window_id) + .unwrap() + .0 + .clone(); + let dispatch_path = presenter.borrow().dispatch_path_from(view_id); + self.dispatch_action_any(window_id, &dispatch_path, action); + } + pub fn dispatch_action( &mut self, window_id: usize, diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 4585e321c442feffd7948bf399d2a08bb0929a63..793f41f48782079cf8008cc896587cbb5a50a5bc 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -51,15 +51,21 @@ impl Presenter { } pub fn dispatch_path(&self, app: &AppContext) -> Vec { + if let Some(view_id) = app.focused_view_id(self.window_id) { + self.dispatch_path_from(view_id) + } else { + Vec::new() + } + } + + pub(crate) fn dispatch_path_from(&self, mut view_id: usize) -> Vec { let mut path = Vec::new(); - if let Some(mut view_id) = app.focused_view_id(self.window_id) { - path.push(view_id); - while let Some(parent_id) = self.parents.get(&view_id).copied() { - path.push(parent_id); - view_id = parent_id; - } - path.reverse(); + path.push(view_id); + while let Some(parent_id) = self.parents.get(&view_id).copied() { + path.push(parent_id); + view_id = parent_id; } + path.reverse(); path } diff --git a/crates/workspace/src/menu.rs b/crates/workspace/src/menu.rs index 33de4a677a3d0503c4dee46243ec1841bbaa449a..81716028b970e56aabcb77b11032ba325b0aaf38 100644 --- a/crates/workspace/src/menu.rs +++ b/crates/workspace/src/menu.rs @@ -1,4 +1,11 @@ gpui::actions!( menu, - [Confirm, SelectPrev, SelectNext, SelectFirst, SelectLast] + [ + Cancel, + Confirm, + SelectPrev, + SelectNext, + SelectFirst, + SelectLast + ] ); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 05efff0ebd64b397cd707ab3de19ca9e59175325..24a06b9f6744e5a8cad39e122d59f4be6198a403 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -33,6 +33,7 @@ assets = { path = "../assets" } breadcrumbs = { path = "../breadcrumbs" } chat_panel = { path = "../chat_panel" } collections = { path = "../collections" } +command_palette = { path = "../command_palette" } client = { path = "../client" } clock = { path = "../clock" } contacts_panel = { path = "../contacts_panel" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 41e27eb49425513fc9377642b4af7a4f60eb31dd..cead3ac39040f2371ed8e3d3908820b1bf1d0afb 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -98,6 +98,7 @@ fn main() { project::Project::init(&client); client::Channel::init(&client); client::init(client.clone(), cx); + command_palette::init(cx); workspace::init(&client, cx); editor::init(cx); go_to_line::init(cx); From 3901d9d544496af7752d13a7e6ea8ecfdc185f28 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 14 Apr 2022 13:50:00 -0700 Subject: [PATCH 03/18] Display key bindings in the command palette They still need to be styled. Co-authored-by: Antonio Scandurra --- crates/command_palette/src/command_palette.rs | 91 +++++++++++++++---- crates/gpui/src/app.rs | 16 +++- crates/gpui/src/keymap.rs | 55 +++++++++-- 3 files changed, 132 insertions(+), 30 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 7e1c2b43b1be72b2d5c9780ab9e599138c0ae501..d8b83ce328a9e74a5bb3ad12d9e3451d17b50f96 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -1,13 +1,13 @@ -use std::cmp; - use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, - elements::{ChildView, Label}, + elements::{ChildView, Flex, Label, ParentElement}, + keymap::Keystroke, Action, Element, Entity, MutableAppContext, View, ViewContext, ViewHandle, }; use selector::{SelectorModal, SelectorModalDelegate}; use settings::Settings; +use std::cmp; use workspace::Workspace; mod selector; @@ -21,7 +21,7 @@ actions!(command_palette, [Toggle]); pub struct CommandPalette { selector: ViewHandle>, - actions: Vec<(&'static str, Box)>, + actions: Vec, matches: Vec, selected_ix: usize, focused_view_id: usize, @@ -31,13 +31,27 @@ pub enum Event { Dismissed, } +struct Command { + name: &'static str, + action: Box, + keystrokes: Vec, + has_multiple_bindings: bool, +} + impl CommandPalette { - pub fn new( - focused_view_id: usize, - actions: Vec<(&'static str, Box)>, - cx: &mut ViewContext, - ) -> Self { + pub fn new(focused_view_id: usize, cx: &mut ViewContext) -> Self { let this = cx.weak_handle(); + let actions = cx + .available_actions(cx.window_id(), focused_view_id) + .map(|(name, action, bindings)| Command { + name, + action, + keystrokes: bindings + .last() + .map_or(Vec::new(), |binding| binding.keystrokes().to_vec()), + has_multiple_bindings: bindings.len() > 1, + }) + .collect(); let selector = cx.add_view(|cx| SelectorModal::new(this, cx)); Self { selector, @@ -54,12 +68,11 @@ impl CommandPalette { let focused_view_id = cx.focused_view_id(window_id).unwrap_or(workspace.id()); cx.as_mut().defer(move |cx| { - let actions = cx.available_actions(window_id, focused_view_id); + let this = cx.add_view(window_id, |cx| Self::new(focused_view_id, cx)); workspace.update(cx, |workspace, cx| { workspace.toggle_modal(cx, |cx, _| { - let selector = cx.add_view(|cx| Self::new(focused_view_id, actions, cx)); - cx.subscribe(&selector, Self::on_event).detach(); - selector + cx.subscribe(&this, Self::on_event).detach(); + this }); }); }); @@ -119,10 +132,10 @@ impl SelectorModalDelegate for CommandPalette { .actions .iter() .enumerate() - .map(|(ix, (name, _))| StringMatchCandidate { + .map(|(ix, command)| StringMatchCandidate { id: ix, - string: name.to_string(), - char_bag: name.chars().collect(), + string: command.name.to_string(), + char_bag: command.name.chars().collect(), }) .collect::>(); cx.spawn(move |this, mut cx| async move { @@ -157,13 +170,15 @@ impl SelectorModalDelegate for CommandPalette { cx.dispatch_action_at( window_id, self.focused_view_id, - self.actions[action_ix].1.as_ref(), + self.actions[action_ix].action.as_ref(), ) } cx.emit(Event::Dismissed); } fn render_match(&self, ix: usize, selected: bool, cx: &gpui::AppContext) -> gpui::ElementBox { + let mat = &self.matches[ix]; + let command = &self.actions[mat.candidate_id]; let settings = cx.global::(); let theme = &settings.theme.selector; let style = if selected { @@ -171,9 +186,49 @@ impl SelectorModalDelegate for CommandPalette { } else { &theme.item }; - Label::new(self.matches[ix].string.clone(), style.label.clone()) + + Flex::row() + .with_child(Label::new(mat.string.clone(), style.label.clone()).boxed()) + .with_children(command.keystrokes.iter().map(|keystroke| { + Flex::row() + .with_children( + [ + (keystroke.ctrl, "^"), + (keystroke.alt, "⎇"), + (keystroke.cmd, "⌘"), + (keystroke.shift, "⇧"), + ] + .into_iter() + .filter_map(|(modifier, label)| { + if modifier { + Some(Label::new(label.into(), style.label.clone()).boxed()) + } else { + None + } + }), + ) + .with_child(Label::new(keystroke.key.clone(), style.label.clone()).boxed()) + .contained() + .with_margin_left(5.0) + .flex_float() + .boxed() + })) + .with_children(if command.has_multiple_bindings { + Some(Label::new("+".into(), style.label.clone()).boxed()) + } else { + None + }) .contained() .with_style(style.container) .boxed() } } + +impl std::fmt::Debug for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Command") + .field("name", &self.name) + .field("keystrokes", &self.keystrokes) + .finish() + } +} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f8721770820ce73f44fa771c200578e7df30a5fc..683ea46999297e162ba8fa4228b6b9a917b290c0 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3,7 +3,7 @@ pub mod action; use crate::{ elements::ElementBox, executor::{self, Task}, - keymap::{self, Keystroke}, + keymap::{self, Binding, Keystroke}, platform::{self, CursorStyle, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::post_inc, @@ -17,6 +17,7 @@ use lazy_static::lazy_static; use parking_lot::Mutex; use platform::Event; use postage::oneshot; +use smallvec::SmallVec; use smol::prelude::*; use std::{ any::{type_name, Any, TypeId}, @@ -1309,7 +1310,7 @@ impl MutableAppContext { &self, window_id: usize, view_id: usize, - ) -> Vec<(&'static str, Box)> { + ) -> impl Iterator, SmallVec<[&Binding; 1]>)> { let mut action_types: HashSet<_> = self.global_actions.keys().copied().collect(); let presenter = self @@ -1330,14 +1331,19 @@ impl MutableAppContext { self.action_deserializers .iter() - .filter_map(|(name, (type_id, deserialize))| { + .filter_map(move |(name, (type_id, deserialize))| { if action_types.contains(type_id) { - Some((*name, deserialize("{}").ok()?)) + Some(( + *name, + deserialize("{}").ok()?, + self.keystroke_matcher + .bindings_for_action_type(*type_id) + .collect(), + )) } else { None } }) - .collect() } pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: &dyn Action) { diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 723403f16012641924051ac4dd5d80b61cfbbb6f..c42fbff907061e5e6813da2411477b62a697c28c 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -1,7 +1,8 @@ use crate::Action; use anyhow::{anyhow, Result}; +use smallvec::SmallVec; use std::{ - any::Any, + any::{Any, TypeId}, collections::{HashMap, HashSet}, fmt::Debug, }; @@ -23,7 +24,10 @@ struct Pending { } #[derive(Default)] -pub struct Keymap(Vec); +pub struct Keymap { + bindings: Vec, + binding_indices_by_action_type: HashMap>, +} pub struct Binding { keystrokes: Vec, @@ -111,6 +115,10 @@ impl Matcher { self.keymap.clear(); } + pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator { + self.keymap.bindings_for_action_type(action_type) + } + pub fn clear_pending(&mut self) { self.pending.clear(); } @@ -132,7 +140,7 @@ impl Matcher { pending.keystrokes.push(keystroke); let mut retain_pending = false; - for binding in self.keymap.0.iter().rev() { + for binding in self.keymap.bindings.iter().rev() { if binding.keystrokes.starts_with(&pending.keystrokes) && binding.context.as_ref().map(|c| c.eval(cx)).unwrap_or(true) { @@ -163,15 +171,44 @@ impl Default for Matcher { impl Keymap { pub fn new(bindings: Vec) -> Self { - Self(bindings) + 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<'a>( + &'a self, + action_type: TypeId, + ) -> impl Iterator { + self.binding_indices_by_action_type + .get(&action_type) + .map(SmallVec::as_slice) + .unwrap_or(&[]) + .iter() + .map(|ix| &self.bindings[*ix]) } fn add_bindings>(&mut self, bindings: T) { - self.0.extend(bindings.into_iter()); + 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.0.clear(); + self.bindings.clear(); + self.binding_indices_by_action_type.clear(); } } @@ -198,6 +235,10 @@ impl Binding { context, }) } + + pub fn keystrokes(&self) -> &[Keystroke] { + &self.keystrokes + } } impl Keystroke { @@ -446,7 +487,7 @@ mod tests { a: &'static str, } - let keymap = Keymap(vec![ + 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")), From bde52d5c938c2770dbaf026d2e08b11cab50b156 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 14 Apr 2022 16:04:32 -0700 Subject: [PATCH 04/18] Add basic styling to keystrokes in command palette --- assets/themes/dark.json | 23 ++++++++++++++ assets/themes/light.json | 23 ++++++++++++++ crates/command_palette/src/command_palette.rs | 31 +++++++++++-------- crates/theme/src/theme.rs | 9 +++++- styles/src/styleTree/app.ts | 2 ++ styles/src/styleTree/commandPalette.ts | 21 +++++++++++++ styles/src/styleTree/selectorModal.ts | 2 +- 7 files changed, 96 insertions(+), 15 deletions(-) create mode 100644 styles/src/styleTree/commandPalette.ts diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 96f1ee0efc64e9c95536dcbf4086c05663509702..e8a082d2caa021f5fc61ccd455e0facc3a4cd719 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -708,6 +708,29 @@ } } }, + "command_palette": { + "keystroke_spacing": 8, + "key": { + "text": { + "family": "Zed Mono", + "color": "#9c9c9c", + "size": 12 + }, + "corner_radius": 3, + "background": "#2472f2", + "border": { + "color": "#2472f2", + "width": 1 + }, + "padding": { + "left": 3, + "right": 3 + }, + "margin": { + "left": 3 + } + } + }, "project_panel": { "padding": { "top": 6, diff --git a/assets/themes/light.json b/assets/themes/light.json index c942dbb39b99a1b72e1cfea404ce05dab2e3dbad..bf9b287a379d7e75dfd0d08aadaa1cc9ea08a15d 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -708,6 +708,29 @@ } } }, + "command_palette": { + "keystroke_spacing": 8, + "key": { + "text": { + "family": "Zed Mono", + "color": "#474747", + "size": 12 + }, + "corner_radius": 3, + "background": "#c5dafc", + "border": { + "color": "#9ec1fa", + "width": 1 + }, + "padding": { + "left": 3, + "right": 3 + }, + "margin": { + "left": 3 + } + } + }, "project_panel": { "padding": { "top": 6, diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index d8b83ce328a9e74a5bb3ad12d9e3451d17b50f96..b75c54bdf085ce43da3ec8327e2df69668def2b1 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -35,7 +35,6 @@ struct Command { name: &'static str, action: Box, keystrokes: Vec, - has_multiple_bindings: bool, } impl CommandPalette { @@ -49,7 +48,6 @@ impl CommandPalette { keystrokes: bindings .last() .map_or(Vec::new(), |binding| binding.keystrokes().to_vec()), - has_multiple_bindings: bindings.len() > 1, }) .collect(); let selector = cx.add_view(|cx| SelectorModal::new(this, cx)); @@ -180,12 +178,14 @@ impl SelectorModalDelegate for CommandPalette { let mat = &self.matches[ix]; let command = &self.actions[mat.candidate_id]; let settings = cx.global::(); - let theme = &settings.theme.selector; + let theme = &settings.theme; let style = if selected { - &theme.active_item + &theme.selector.active_item } else { - &theme.item + &theme.selector.item }; + let key_style = &theme.command_palette.key; + let keystroke_spacing = theme.command_palette.keystroke_spacing; Flex::row() .with_child(Label::new(mat.string.clone(), style.label.clone()).boxed()) @@ -201,23 +201,28 @@ impl SelectorModalDelegate for CommandPalette { .into_iter() .filter_map(|(modifier, label)| { if modifier { - Some(Label::new(label.into(), style.label.clone()).boxed()) + Some( + Label::new(label.into(), key_style.label.clone()) + .contained() + .with_style(key_style.container) + .boxed(), + ) } else { None } }), ) - .with_child(Label::new(keystroke.key.clone(), style.label.clone()).boxed()) + .with_child( + Label::new(keystroke.key.clone(), key_style.label.clone()) + .contained() + .with_style(key_style.container) + .boxed(), + ) .contained() - .with_margin_left(5.0) + .with_margin_left(keystroke_spacing) .flex_float() .boxed() })) - .with_children(if command.has_multiple_bindings { - Some(Label::new("+".into(), style.label.clone()).boxed()) - } else { - None - }) .contained() .with_style(style.container) .boxed() diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index b3c239b4b9f119159f76a3ed23ec477069c6bbd8..c831738423af54b16dada92589838667dc819938 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -21,6 +21,7 @@ pub struct Theme { pub chat_panel: ChatPanel, pub contacts_panel: ContactsPanel, pub project_panel: ProjectPanel, + pub command_palette: CommandPalette, pub selector: Selector, pub editor: Editor, pub search: Search, @@ -187,6 +188,12 @@ pub struct ProjectPanelEntry { pub icon_spacing: f32, } +#[derive(Debug, Deserialize, Default)] +pub struct CommandPalette { + pub key: ContainedLabel, + pub keystroke_spacing: f32, +} + #[derive(Deserialize, Default)] pub struct ContactsPanel { #[serde(flatten)] @@ -259,7 +266,7 @@ pub struct ContainedText { pub text: TextStyle, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Debug, Deserialize, Default)] pub struct ContainedLabel { #[serde(flatten)] pub container: ContainerStyle, diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 03e527d03dec98755e84b789ad016a7aa67a3306..1f98f8ba9c52bbf01e3f61c24175a9e463478b60 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -2,6 +2,7 @@ import Theme from "../themes/theme"; import chatPanel from "./chatPanel"; import { text } from "./components"; import contactsPanel from "./contactsPanel"; +import commandPalette from "./commandPalette"; import editor from "./editor"; import projectPanel from "./projectPanel"; import search from "./search"; @@ -29,6 +30,7 @@ export default function app(theme: Theme): Object { }, }, }, + commandPalette: commandPalette(theme), projectPanel: projectPanel(theme), chatPanel: chatPanel(theme), contactsPanel: contactsPanel(theme), diff --git a/styles/src/styleTree/commandPalette.ts b/styles/src/styleTree/commandPalette.ts new file mode 100644 index 0000000000000000000000000000000000000000..f7a0f51f6eb2949914cbda235a571e549c6e8420 --- /dev/null +++ b/styles/src/styleTree/commandPalette.ts @@ -0,0 +1,21 @@ +import Theme from "../themes/theme"; +import { text, backgroundColor, border } from "./components"; + +export default function commandPalette(theme: Theme) { + return { + keystrokeSpacing: 8, + key: { + text: text(theme, "mono", "secondary", { size: "xs" }), + cornerRadius: 3, + background: backgroundColor(theme, "info", "base"), + border: border(theme, "info"), + padding: { + left: 3, + right: 3, + }, + margin: { + left: 3 + }, + } + } +} diff --git a/styles/src/styleTree/selectorModal.ts b/styles/src/styleTree/selectorModal.ts index 90570cc09374290dd842296e96f97ce0127be05d..1bb44a776fb7382b205ce7bd45db85afcd7aab47 100644 --- a/styles/src/styleTree/selectorModal.ts +++ b/styles/src/styleTree/selectorModal.ts @@ -38,7 +38,7 @@ export default function selectorModal(theme: Theme): Object { }, inputEditor: { background: backgroundColor(theme, 500), - corner_radius: 6, + cornerRadius: 6, placeholderText: text(theme, "sans", "placeholder"), selection: player(theme, 1).selection, text: text(theme, "mono", "primary"), From 7964464e3d465977ac4aeafa684c9b0bd1a0d514 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 14 Apr 2022 16:21:22 -0700 Subject: [PATCH 05/18] Rename SelectorModal -> Picker, put it in its own crate --- Cargo.lock | 16 ++++++++++ crates/command_palette/Cargo.toml | 1 + crates/command_palette/src/command_palette.rs | 12 +++---- crates/picker/Cargo.toml | 23 +++++++++++++ .../src/selector.rs => picker/src/picker.rs} | 32 +++++++++---------- 5 files changed, 61 insertions(+), 23 deletions(-) create mode 100644 crates/picker/Cargo.toml rename crates/{command_palette/src/selector.rs => picker/src/picker.rs} (91%) diff --git a/Cargo.lock b/Cargo.lock index 484dab47d84372d794780e4a8811a18b0da16edb..d501ed4b7e7e6301295b4d916a709cd91677ddd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1152,6 +1152,7 @@ dependencies = [ "env_logger 0.8.3", "fuzzy", "gpui", + "picker", "serde_json", "settings", "theme", @@ -3545,6 +3546,21 @@ dependencies = [ "indexmap", ] +[[package]] +name = "picker" +version = "0.1.0" +dependencies = [ + "ctor", + "editor", + "env_logger 0.8.3", + "gpui", + "serde_json", + "settings", + "theme", + "util", + "workspace", +] + [[package]] name = "pico-args" version = "0.4.0" diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index b360a041254a61956bfb5cb2ea92d22e1ec73ba0..ecff82f6f44a455bc1d58d45c120312e37af31a5 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -11,6 +11,7 @@ doctest = false editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +picker = { path = "../picker" } settings = { path = "../settings" } util = { path = "../util" } theme = { path = "../theme" } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index b75c54bdf085ce43da3ec8327e2df69668def2b1..18773aaf9c76752032eb5fd0eb60403d435aafa6 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -5,22 +5,20 @@ use gpui::{ keymap::Keystroke, Action, Element, Entity, MutableAppContext, View, ViewContext, ViewHandle, }; -use selector::{SelectorModal, SelectorModalDelegate}; +use picker::{Picker, PickerDelegate}; use settings::Settings; use std::cmp; use workspace::Workspace; -mod selector; - pub fn init(cx: &mut MutableAppContext) { cx.add_action(CommandPalette::toggle); - selector::init::(cx); + Picker::::init(cx); } actions!(command_palette, [Toggle]); pub struct CommandPalette { - selector: ViewHandle>, + selector: ViewHandle>, actions: Vec, matches: Vec, selected_ix: usize, @@ -50,7 +48,7 @@ impl CommandPalette { .map_or(Vec::new(), |binding| binding.keystrokes().to_vec()), }) .collect(); - let selector = cx.add_view(|cx| SelectorModal::new(this, cx)); + let selector = cx.add_view(|cx| Picker::new(this, cx)); Self { selector, actions, @@ -108,7 +106,7 @@ impl View for CommandPalette { } } -impl SelectorModalDelegate for CommandPalette { +impl PickerDelegate for CommandPalette { fn match_count(&self) -> usize { self.matches.len() } diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..86e657ecad9073dba73d82e7c6d285b0426e79cb --- /dev/null +++ b/crates/picker/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "picker" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/picker.rs" +doctest = false + +[dependencies] +editor = { path = "../editor" } +gpui = { path = "../gpui" } +settings = { path = "../settings" } +util = { path = "../util" } +theme = { path = "../theme" } +workspace = { path = "../workspace" } + +[dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } +serde_json = { version = "1.0.64", features = ["preserve_order"] } +workspace = { path = "../workspace", features = ["test-support"] } +ctor = "0.1" +env_logger = "0.8" diff --git a/crates/command_palette/src/selector.rs b/crates/picker/src/picker.rs similarity index 91% rename from crates/command_palette/src/selector.rs rename to crates/picker/src/picker.rs index daa0f5ff6a1a286d4304ef56272abf3ee9d908ed..797f681d42a97d16a3caebd359a687c04b10f7ea 100644 --- a/crates/command_palette/src/selector.rs +++ b/crates/picker/src/picker.rs @@ -11,22 +11,13 @@ use settings::Settings; use std::cmp; use workspace::menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; -pub fn init(cx: &mut MutableAppContext) { - cx.add_action(SelectorModal::::select_first); - cx.add_action(SelectorModal::::select_last); - cx.add_action(SelectorModal::::select_next); - cx.add_action(SelectorModal::::select_prev); - cx.add_action(SelectorModal::::confirm); - cx.add_action(SelectorModal::::cancel); -} - -pub struct SelectorModal { +pub struct Picker { delegate: WeakViewHandle, query_editor: ViewHandle, list_state: UniformListState, } -pub trait SelectorModalDelegate: View { +pub trait PickerDelegate: View { fn match_count(&self) -> usize; fn selected_index(&self) -> usize; fn set_selected_index(&mut self, ix: usize); @@ -36,13 +27,13 @@ pub trait SelectorModalDelegate: View { fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox; } -impl Entity for SelectorModal { +impl Entity for Picker { type Event = (); } -impl View for SelectorModal { +impl View for Picker { fn ui_name() -> &'static str { - "SelectorModal" + "Picker" } fn render(&mut self, cx: &mut RenderContext) -> gpui::ElementBox { @@ -66,7 +57,7 @@ impl View for SelectorModal { .with_max_height(420.0) .aligned() .top() - .named("selector") + .named("picker") } fn keymap_context(&self, _: &AppContext) -> keymap::Context { @@ -80,7 +71,16 @@ impl View for SelectorModal { } } -impl SelectorModal { +impl Picker { + pub fn init(cx: &mut MutableAppContext) { + cx.add_action(Self::select_first); + cx.add_action(Self::select_last); + cx.add_action(Self::select_next); + cx.add_action(Self::select_prev); + cx.add_action(Self::confirm); + cx.add_action(Self::cancel); + } + pub fn new(delegate: WeakViewHandle, cx: &mut ViewContext) -> Self { let query_editor = cx.add_view(|cx| { Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx) From 7b168608063a5531a3a9fe4211ebf02f06e50dd7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 14 Apr 2022 16:51:14 -0700 Subject: [PATCH 06/18] Use Picker in ThemeSelector --- Cargo.lock | 1 + crates/command_palette/src/command_palette.rs | 12 +- crates/picker/src/picker.rs | 16 +- crates/theme_selector/Cargo.toml | 1 + crates/theme_selector/src/theme_selector.rs | 332 +++++++----------- 5 files changed, 136 insertions(+), 226 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d501ed4b7e7e6301295b4d916a709cd91677ddd2..0b5a802c520978a51b542f63373e51c0d5974ba9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5310,6 +5310,7 @@ dependencies = [ "gpui", "log", "parking_lot", + "picker", "postage", "settings", "smol", diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 18773aaf9c76752032eb5fd0eb60403d435aafa6..ddefe4d8308ce114ac5b0f5da78626b2b1c5d58c 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -18,7 +18,7 @@ pub fn init(cx: &mut MutableAppContext) { actions!(command_palette, [Toggle]); pub struct CommandPalette { - selector: ViewHandle>, + picker: ViewHandle>, actions: Vec, matches: Vec, selected_ix: usize, @@ -48,9 +48,9 @@ impl CommandPalette { .map_or(Vec::new(), |binding| binding.keystrokes().to_vec()), }) .collect(); - let selector = cx.add_view(|cx| Picker::new(this, cx)); + let picker = cx.add_view(|cx| Picker::new(this, cx)); Self { - selector, + picker, actions, matches: vec![], selected_ix: 0, @@ -98,11 +98,11 @@ impl View for CommandPalette { } fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - ChildView::new(self.selector.clone()).boxed() + ChildView::new(self.picker.clone()).boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.selector); + cx.focus(&self.picker); } } @@ -115,7 +115,7 @@ impl PickerDelegate for CommandPalette { self.selected_ix } - fn set_selected_index(&mut self, ix: usize) { + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext) { self.selected_ix = ix; } diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 797f681d42a97d16a3caebd359a687c04b10f7ea..913d182aa5f60bdde01afcfd62c7ee6be17363eb 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -20,7 +20,7 @@ pub struct Picker { pub trait PickerDelegate: View { fn match_count(&self) -> usize; fn selected_index(&self) -> usize; - fn set_selected_index(&mut self, ix: usize); + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext); fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> Task<()>; fn confirm(&mut self, cx: &mut ViewContext); fn dismiss(&mut self, cx: &mut ViewContext); @@ -159,7 +159,7 @@ impl Picker { fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { if let Some(delegate) = self.delegate.upgrade(cx) { let index = 0; - delegate.update(cx, |delegate, _| delegate.set_selected_index(0)); + delegate.update(cx, |delegate, cx| delegate.set_selected_index(0, cx)); self.list_state.scroll_to(ScrollTarget::Show(index)); cx.notify(); } @@ -167,10 +167,10 @@ impl Picker { fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { if let Some(delegate) = self.delegate.upgrade(cx) { - let index = delegate.update(cx, |delegate, _| { + let index = delegate.update(cx, |delegate, cx| { let match_count = delegate.match_count(); let index = if match_count > 0 { match_count - 1 } else { 0 }; - delegate.set_selected_index(index); + delegate.set_selected_index(index, cx); index }); self.list_state.scroll_to(ScrollTarget::Show(index)); @@ -180,11 +180,11 @@ impl Picker { fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { if let Some(delegate) = self.delegate.upgrade(cx) { - let index = delegate.update(cx, |delegate, _| { + let index = delegate.update(cx, |delegate, cx| { let mut selected_index = delegate.selected_index(); if selected_index + 1 < delegate.match_count() { selected_index += 1; - delegate.set_selected_index(selected_index); + delegate.set_selected_index(selected_index, cx); } selected_index }); @@ -195,11 +195,11 @@ impl Picker { fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { if let Some(delegate) = self.delegate.upgrade(cx) { - let index = delegate.update(cx, |delegate, _| { + let index = delegate.update(cx, |delegate, cx| { let mut selected_index = delegate.selected_index(); if selected_index > 0 { selected_index -= 1; - delegate.set_selected_index(selected_index); + delegate.set_selected_index(selected_index, cx); } selected_index }); diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index 9993287c8580b65870e20736c51ea66ca87f90c6..804eff2c7a6cbab7035811391f17ff63ad34a8b8 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -11,6 +11,7 @@ doctest = false editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +picker = { path = "../picker" } theme = { path = "../theme" } settings = { path = "../settings" } workspace = { path = "../workspace" } diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 37099eb510a9b476d59c396dd3b564a6acb581e8..f1e933774f8e0dfab2ff6b65b30fb5bb465432c7 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -1,35 +1,30 @@ -use editor::Editor; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - actions, elements::*, keymap, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, + actions, elements::*, AppContext, Element, ElementBox, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; +use picker::{Picker, PickerDelegate}; use settings::Settings; -use std::{cmp, sync::Arc}; +use std::sync::Arc; use theme::{Theme, ThemeRegistry}; -use workspace::{ - menu::{Confirm, SelectNext, SelectPrev}, - Workspace, -}; +use workspace::Workspace; pub struct ThemeSelector { - themes: Arc, + registry: Arc, + theme_names: Vec, matches: Vec, - query_editor: ViewHandle, - list_state: UniformListState, - selected_index: usize, original_theme: Arc, + picker: ViewHandle>, selection_completed: bool, + selected_index: usize, } actions!(theme_selector, [Toggle, Reload]); pub fn init(cx: &mut MutableAppContext) { - cx.add_action(ThemeSelector::confirm); - cx.add_action(ThemeSelector::select_prev); - cx.add_action(ThemeSelector::select_next); cx.add_action(ThemeSelector::toggle); cx.add_action(ThemeSelector::reload); + Picker::::init(cx); } pub enum Event { @@ -38,38 +33,38 @@ pub enum Event { impl ThemeSelector { fn new(registry: Arc, cx: &mut ViewContext) -> Self { - let query_editor = cx.add_view(|cx| { - Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx) - }); - - cx.subscribe(&query_editor, Self::on_query_editor_event) - .detach(); - + let handle = cx.weak_handle(); + let picker = cx.add_view(|cx| Picker::new(handle, cx)); let original_theme = cx.global::().theme.clone(); - + let theme_names = registry.list().collect::>(); + let matches = theme_names + .iter() + .map(|name| StringMatch { + candidate_id: 0, + score: 0.0, + positions: Default::default(), + string: name.clone(), + }) + .collect(); let mut this = Self { - themes: registry, - query_editor, - matches: Vec::new(), - list_state: Default::default(), - selected_index: 0, // Default index for now + registry, + theme_names, + matches, + picker, original_theme: original_theme.clone(), + selected_index: 0, selection_completed: false, }; - this.update_matches(cx); - - // Set selected index to current theme this.select_if_matching(&original_theme.name); - this } fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { let themes = workspace.themes(); workspace.toggle_modal(cx, |cx, _| { - let selector = cx.add_view(|cx| Self::new(themes, cx)); - cx.subscribe(&selector, Self::on_event).detach(); - selector + let this = cx.add_view(|cx| Self::new(themes, cx)); + cx.subscribe(&this, Self::on_event).detach(); + this }); } @@ -88,36 +83,9 @@ impl ThemeSelector { } } - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - self.selection_completed = true; - cx.emit(Event::Dismissed); - } - - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if self.selected_index > 0 { - self.selected_index -= 1; - } - self.list_state - .scroll_to(ScrollTarget::Show(self.selected_index)); - - self.show_selected_theme(cx); - cx.notify(); - } - - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - if self.selected_index + 1 < self.matches.len() { - self.selected_index += 1; - } - self.list_state - .scroll_to(ScrollTarget::Show(self.selected_index)); - - self.show_selected_theme(cx); - cx.notify(); - } - fn show_selected_theme(&mut self, cx: &mut ViewContext) { if let Some(mat) = self.matches.get(self.selected_index) { - match self.themes.get(&mat.string) { + match self.registry.get(&mat.string) { Ok(theme) => Self::set_theme(theme, cx), Err(error) => { log::error!("error loading theme {}: {}", mat.string, error) @@ -134,49 +102,6 @@ impl ThemeSelector { .unwrap_or(self.selected_index); } - fn update_matches(&mut self, cx: &mut ViewContext) { - let background = cx.background().clone(); - let candidates = self - .themes - .list() - .enumerate() - .map(|(id, name)| StringMatchCandidate { - id, - char_bag: name.as_str().into(), - string: name, - }) - .collect::>(); - let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); - - self.matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - smol::block_on(match_strings( - &candidates, - &query, - false, - 100, - &Default::default(), - background, - )) - }; - - self.selected_index = self - .selected_index - .min(self.matches.len().saturating_sub(1)); - - cx.notify(); - } - fn on_event( workspace: &mut Workspace, _: ViewHandle, @@ -190,89 +115,104 @@ impl ThemeSelector { } } - fn on_query_editor_event( - &mut self, - _: ViewHandle, - event: &editor::Event, - cx: &mut ViewContext, - ) { - match event { - editor::Event::BufferEdited { .. } => { - self.update_matches(cx); - self.select_if_matching(&cx.global::().theme.name); - self.show_selected_theme(cx); - } - editor::Event::Blurred => cx.emit(Event::Dismissed), - _ => {} - } + fn set_theme(theme: Arc, cx: &mut MutableAppContext) { + cx.update_global::(|settings, cx| { + settings.theme = theme; + cx.refresh_windows(); + }); } +} - fn render_matches(&self, cx: &mut RenderContext) -> ElementBox { - if self.matches.is_empty() { - let settings = cx.global::(); - return Container::new( - Label::new( - "No matches".into(), - settings.theme.selector.empty.label.clone(), - ) - .boxed(), - ) - .with_style(settings.theme.selector.empty.container) - .named("empty matches"); +impl PickerDelegate for ThemeSelector { + fn match_count(&self) -> usize { + self.matches.len() + } + + fn confirm(&mut self, cx: &mut ViewContext) { + self.selection_completed = true; + cx.emit(Event::Dismissed); + } + + fn dismiss(&mut self, cx: &mut ViewContext) { + if !self.selection_completed { + Self::set_theme(self.original_theme.clone(), cx); + self.selection_completed = true; } + cx.emit(Event::Dismissed); + } - let handle = cx.handle(); - let list = - UniformList::new( - self.list_state.clone(), - self.matches.len(), - move |mut range, items, cx| { - let cx = cx.as_ref(); - let selector = handle.upgrade(cx).unwrap(); - let selector = selector.read(cx); - let start = range.start; - range.end = cmp::min(range.end, selector.matches.len()); - items.extend(selector.matches[range].iter().enumerate().map( - move |(i, path_match)| selector.render_match(path_match, start + i, cx), - )); - }, - ); + fn selected_index(&self) -> usize { + self.selected_index + } - Container::new(list.boxed()) - .with_margin_top(6.0) - .named("matches") + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + self.selected_index = ix; + self.show_selected_theme(cx); } - fn render_match(&self, theme_match: &StringMatch, index: usize, cx: &AppContext) -> ElementBox { - let settings = cx.global::(); - let theme = &settings.theme; + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> gpui::Task<()> { + let background = cx.background().clone(); + let candidates = self + .theme_names + .iter() + .enumerate() + .map(|(id, name)| StringMatchCandidate { + id, + char_bag: name.as_str().into(), + string: name.clone(), + }) + .collect::>(); - let container = Container::new( - Label::new( - theme_match.string.clone(), - if index == self.selected_index { - theme.selector.active_item.label.clone() - } else { - theme.selector.item.label.clone() - }, - ) - .with_highlights(theme_match.positions.clone()) - .boxed(), - ) - .with_style(if index == self.selected_index { - theme.selector.active_item.container - } else { - theme.selector.item.container - }); + cx.spawn(|this, mut cx| async move { + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + ) + .await + }; - container.boxed() + this.update(&mut cx, |this, cx| { + this.matches = matches; + this.selected_index = this + .selected_index + .min(this.matches.len().saturating_sub(1)); + this.show_selected_theme(cx); + cx.notify(); + }); + }) } - fn set_theme(theme: Arc, cx: &mut MutableAppContext) { - cx.update_global::(|settings, cx| { - settings.theme = theme; - cx.refresh_windows(); - }); + fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox { + let settings = cx.global::(); + let theme = &settings.theme; + let theme_match = &self.matches[ix]; + let style = if selected { + &theme.selector.active_item + } else { + &theme.selector.item + }; + + Label::new(theme_match.string.clone(), style.label.clone()) + .with_highlights(theme_match.positions.clone()) + .contained() + .with_style(style.container) + .boxed() } } @@ -291,43 +231,11 @@ impl View for ThemeSelector { "ThemeSelector" } - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.global::().theme.clone(); - Align::new( - ConstrainedBox::new( - Container::new( - Flex::new(Axis::Vertical) - .with_child( - ChildView::new(&self.query_editor) - .contained() - .with_style(theme.selector.input_editor.container) - .boxed(), - ) - .with_child( - FlexItem::new(self.render_matches(cx)) - .flex(1., false) - .boxed(), - ) - .boxed(), - ) - .with_style(theme.selector.container) - .boxed(), - ) - .with_max_width(600.0) - .with_max_height(400.0) - .boxed(), - ) - .top() - .named("theme selector") + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + ChildView::new(self.picker.clone()).boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.query_editor); - } - - fn keymap_context(&self, _: &AppContext) -> keymap::Context { - let mut cx = Self::default_keymap_context(); - cx.set.insert("menu".into()); - cx + cx.focus(&self.picker); } } From c75ffc583cf532309de3ccb82c590639ab41f2b5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 14 Apr 2022 17:35:32 -0700 Subject: [PATCH 07/18] Use Picker in ProjectSymbolsView --- Cargo.lock | 1 + crates/picker/src/picker.rs | 46 ++- crates/project_symbols/Cargo.toml | 1 + crates/project_symbols/src/project_symbols.rs | 331 ++++++------------ 4 files changed, 145 insertions(+), 234 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b5a802c520978a51b542f63373e51c0d5974ba9..ed7c23bab51b76dc8d8787854c6d89bdc825ab30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3794,6 +3794,7 @@ dependencies = [ "fuzzy", "gpui", "ordered-float", + "picker", "postage", "project", "settings", diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 913d182aa5f60bdde01afcfd62c7ee6be17363eb..38bcdeda307fb9f6a541fdd5667124336296e0fb 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -15,6 +15,7 @@ pub struct Picker { delegate: WeakViewHandle, query_editor: ViewHandle, list_state: UniformListState, + update_task: Option>, } pub trait PickerDelegate: View { @@ -87,12 +88,14 @@ impl Picker { }); cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); - - Self { - delegate, + let mut this = Self { query_editor, list_state: Default::default(), - } + update_task: None, + delegate, + }; + this.update_matches(cx); + this } fn render_matches(&self, cx: &AppContext) -> ElementBox { @@ -137,22 +140,31 @@ impl Picker { event: &editor::Event, cx: &mut ViewContext, ) { - if let Some(delegate) = self.delegate.upgrade(cx) { - match event { - editor::Event::BufferEdited { .. } => { - let query = self.query_editor.read(cx).text(cx); - let update = delegate.update(cx, |d, cx| d.update_matches(query, cx)); - cx.spawn(|this, mut cx| async move { - update.await; - this.update(&mut cx, |_, cx| cx.notify()); + match event { + editor::Event::BufferEdited { .. } => self.update_matches(cx), + editor::Event::Blurred => { + if let Some(delegate) = self.delegate.upgrade(cx) { + delegate.update(cx, |delegate, cx| { + delegate.dismiss(cx); }) - .detach(); } - editor::Event::Blurred => delegate.update(cx, |delegate, cx| { - delegate.dismiss(cx); - }), - _ => {} } + _ => {} + } + } + + fn update_matches(&mut self, cx: &mut ViewContext) { + if let Some(delegate) = self.delegate.upgrade(cx) { + let query = self.query_editor.read(cx).text(cx); + let update = delegate.update(cx, |d, cx| d.update_matches(query, cx)); + cx.notify(); + self.update_task = Some(cx.spawn(|this, mut cx| async move { + update.await; + this.update(&mut cx, |this, cx| { + cx.notify(); + this.update_task.take(); + }); + })); } } diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml index de22c0eda027a4a3ec1e0a6c0f0bc4024fb58747..e199b700f6860db74ce2da5c09b7d0474cb84c5c 100644 --- a/crates/project_symbols/Cargo.toml +++ b/crates/project_symbols/Cargo.toml @@ -11,6 +11,7 @@ doctest = false editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +picker = { path = "../picker" } project = { path = "../project" } text = { path = "../text" } settings = { path = "../settings" } diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index c14f7bea33b70089f3988766e64ea38fe8f1d11e..2c048c4b7e0c6c66c63966712433c6e7e752c14a 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -3,43 +3,32 @@ use editor::{ }; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, elements::*, keymap, AppContext, Axis, Entity, ModelHandle, MutableAppContext, - RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, + actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, + View, ViewContext, ViewHandle, }; use ordered_float::OrderedFloat; +use picker::{Picker, PickerDelegate}; use project::{Project, Symbol}; use settings::Settings; -use std::{ - borrow::Cow, - cmp::{self, Reverse}, -}; +use std::{borrow::Cow, cmp::Reverse}; use util::ResultExt; -use workspace::{ - menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}, - Workspace, -}; +use workspace::Workspace; actions!(project_symbols, [Toggle]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectSymbolsView::toggle); - cx.add_action(ProjectSymbolsView::confirm); - cx.add_action(ProjectSymbolsView::select_prev); - cx.add_action(ProjectSymbolsView::select_next); - cx.add_action(ProjectSymbolsView::select_first); - cx.add_action(ProjectSymbolsView::select_last); + Picker::::init(cx); } pub struct ProjectSymbolsView { - handle: WeakViewHandle, + picker: ViewHandle>, project: ModelHandle, selected_match_index: usize, - list_state: UniformListState, symbols: Vec, match_candidates: Vec, + show_worktree_root_name: bool, matches: Vec, - pending_symbols_task: Task>, - query_editor: ViewHandle, } pub enum Event { @@ -56,59 +45,29 @@ impl View for ProjectSymbolsView { "ProjectSymbolsView" } - fn keymap_context(&self, _: &AppContext) -> keymap::Context { - let mut cx = Self::default_keymap_context(); - cx.set.insert("menu".into()); - cx - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let settings = cx.global::(); - Flex::new(Axis::Vertical) - .with_child( - Container::new(ChildView::new(&self.query_editor).boxed()) - .with_style(settings.theme.selector.input_editor.container) - .boxed(), - ) - .with_child( - FlexItem::new(self.render_matches(cx)) - .flex(1., false) - .boxed(), - ) - .contained() - .with_style(settings.theme.selector.container) - .constrained() - .with_max_width(500.0) - .with_max_height(420.0) - .aligned() - .top() - .named("project symbols view") + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + ChildView::new(self.picker.clone()).boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.query_editor); + cx.focus(&self.picker); } } impl ProjectSymbolsView { fn new(project: ModelHandle, cx: &mut ViewContext) -> Self { - let query_editor = cx.add_view(|cx| { - Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx) - }); - cx.subscribe(&query_editor, Self::on_query_editor_event) - .detach(); + let handle = cx.weak_handle(); + let picker = cx.add_view(|cx| Picker::new(handle, cx)); let mut this = Self { - handle: cx.weak_handle(), + picker, project, selected_match_index: 0, - list_state: Default::default(), symbols: Default::default(), match_candidates: Default::default(), matches: Default::default(), - pending_symbols_task: Task::ready(None), - query_editor, + show_worktree_root_name: false, }; - this.update_matches(cx); + this.update_matches(String::new(), cx).detach(); this } @@ -121,72 +80,7 @@ impl ProjectSymbolsView { }); } - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if self.selected_match_index > 0 { - self.select(self.selected_match_index - 1, cx); - } - } - - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - if self.selected_match_index + 1 < self.matches.len() { - self.select(self.selected_match_index + 1, cx); - } - } - - fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { - self.select(0, cx); - } - - fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { - self.select(self.matches.len().saturating_sub(1), cx); - } - - fn select(&mut self, index: usize, cx: &mut ViewContext) { - self.selected_match_index = index; - self.list_state.scroll_to(ScrollTarget::Show(index)); - cx.notify(); - } - - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some(symbol) = self - .matches - .get(self.selected_match_index) - .map(|mat| self.symbols[mat.candidate_id].clone()) - { - cx.emit(Event::Selected(symbol)); - } - } - - fn update_matches(&mut self, cx: &mut ViewContext) { - self.filter(cx); - let query = self.query_editor.read(cx).text(cx); - let symbols = self - .project - .update(cx, |project, cx| project.symbols(&query, cx)); - self.pending_symbols_task = cx.spawn_weak(|this, mut cx| async move { - let symbols = symbols.await.log_err()?; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.match_candidates = symbols - .iter() - .enumerate() - .map(|(id, symbol)| { - StringMatchCandidate::new( - id, - symbol.label.text[symbol.label.filter_range.clone()].to_string(), - ) - }) - .collect(); - this.symbols = symbols; - this.filter(cx); - }); - } - None - }); - } - - fn filter(&mut self, cx: &mut ViewContext) { - let query = self.query_editor.read(cx).text(cx); + fn filter(&mut self, query: &str, cx: &mut ViewContext) { let mut matches = if query.is_empty() { self.match_candidates .iter() @@ -201,7 +95,7 @@ impl ProjectSymbolsView { } else { smol::block_on(fuzzy::match_strings( &self.match_candidates, - &query, + query, false, 100, &Default::default(), @@ -225,57 +119,111 @@ impl ProjectSymbolsView { } self.matches = matches; - self.select_first(&SelectFirst, cx); + self.set_selected_index(0, cx); cx.notify(); } - fn render_matches(&self, cx: &AppContext) -> ElementBox { - if self.matches.is_empty() { - let settings = cx.global::(); - return Container::new( - Label::new( - "No matches".into(), - settings.theme.selector.empty.label.clone(), - ) - .boxed(), - ) - .with_style(settings.theme.selector.empty.container) - .named("empty matches"); + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => workspace.dismiss_modal(cx), + Event::Selected(symbol) => { + let buffer = workspace + .project() + .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx)); + + let symbol = symbol.clone(); + cx.spawn(|workspace, mut cx| async move { + let buffer = buffer.await?; + workspace.update(&mut cx, |workspace, cx| { + let position = buffer + .read(cx) + .clip_point_utf16(symbol.range.start, Bias::Left); + + let editor = workspace.open_project_item::(buffer, cx); + editor.update(cx, |editor, cx| { + editor.select_ranges( + [position..position], + Some(Autoscroll::Center), + cx, + ); + }); + }); + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + workspace.dismiss_modal(cx); + } } + } +} - let handle = self.handle.clone(); - let list = UniformList::new( - self.list_state.clone(), - self.matches.len(), - move |mut range, items, cx| { - let cx = cx.as_ref(); - let view = handle.upgrade(cx).unwrap(); - let view = view.read(cx); - let start = range.start; - range.end = cmp::min(range.end, view.matches.len()); +impl PickerDelegate for ProjectSymbolsView { + fn confirm(&mut self, cx: &mut ViewContext) { + if let Some(symbol) = self + .matches + .get(self.selected_match_index) + .map(|mat| self.symbols[mat.candidate_id].clone()) + { + cx.emit(Event::Selected(symbol)); + } + } + + fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Dismissed); + } - let show_worktree_root_name = - view.project.read(cx).visible_worktrees(cx).count() > 1; - items.extend(view.matches[range].iter().enumerate().map(move |(ix, m)| { - view.render_match(m, start + ix, show_worktree_root_name, cx) - })); - }, - ); + fn match_count(&self) -> usize { + self.matches.len() + } - Container::new(list.boxed()) - .with_margin_top(6.0) - .named("matches") + fn selected_index(&self) -> usize { + self.selected_match_index } - fn render_match( - &self, - string_match: &StringMatch, - index: usize, - show_worktree_root_name: bool, - cx: &AppContext, - ) -> ElementBox { + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + self.selected_match_index = ix; + cx.notify(); + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { + self.filter(&query, cx); + self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1; + let symbols = self + .project + .update(cx, |project, cx| project.symbols(&query, cx)); + cx.spawn_weak(|this, mut cx| async move { + let symbols = symbols.await.log_err(); + if let Some(this) = this.upgrade(&cx) { + if let Some(symbols) = symbols { + this.update(&mut cx, |this, cx| { + this.match_candidates = symbols + .iter() + .enumerate() + .map(|(id, symbol)| { + StringMatchCandidate::new( + id, + symbol.label.text[symbol.label.filter_range.clone()] + .to_string(), + ) + }) + .collect(); + this.symbols = symbols; + this.filter(&query, cx); + }); + } + } + }) + } + + fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox { + let string_match = &self.matches[ix]; let settings = cx.global::(); - let style = if index == self.selected_match_index { + let style = if selected { &settings.theme.selector.active_item } else { &settings.theme.selector.item @@ -284,7 +232,7 @@ impl ProjectSymbolsView { let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax); let mut path = symbol.path.to_string_lossy(); - if show_worktree_root_name { + if self.show_worktree_root_name { let project = self.project.read(cx); if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) { path = Cow::Owned(format!( @@ -317,55 +265,4 @@ impl ProjectSymbolsView { .with_style(style.container) .boxed() } - - fn on_query_editor_event( - &mut self, - _: ViewHandle, - event: &editor::Event, - cx: &mut ViewContext, - ) { - match event { - editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::BufferEdited { .. } => self.update_matches(cx), - _ => {} - } - } - - fn on_event( - workspace: &mut Workspace, - _: ViewHandle, - event: &Event, - cx: &mut ViewContext, - ) { - match event { - Event::Dismissed => workspace.dismiss_modal(cx), - Event::Selected(symbol) => { - let buffer = workspace - .project() - .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx)); - - let symbol = symbol.clone(); - cx.spawn(|workspace, mut cx| async move { - let buffer = buffer.await?; - workspace.update(&mut cx, |workspace, cx| { - let position = buffer - .read(cx) - .clip_point_utf16(symbol.range.start, Bias::Left); - - let editor = workspace.open_project_item::(buffer, cx); - editor.update(cx, |editor, cx| { - editor.select_ranges( - [position..position], - Some(Autoscroll::Center), - cx, - ); - }); - }); - Ok::<_, anyhow::Error>(()) - }) - .detach_and_log_err(cx); - workspace.dismiss_modal(cx); - } - } - } } From c7527f92a479fc5642f9799a032a58290250ced6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 14 Apr 2022 18:16:04 -0700 Subject: [PATCH 08/18] Use Picker in FileFinder --- Cargo.lock | 1 + crates/file_finder/Cargo.toml | 1 + crates/file_finder/src/file_finder.rs | 359 +++++++++----------------- crates/picker/src/picker.rs | 16 +- 4 files changed, 130 insertions(+), 247 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed7c23bab51b76dc8d8787854c6d89bdc825ab30..a2d65dd9a5c5103f441e4b0ff5a5e2cef45a899d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1907,6 +1907,7 @@ dependencies = [ "env_logger 0.8.3", "fuzzy", "gpui", + "picker", "postage", "project", "serde_json", diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 47dd9b15bc07423c9cb4a86ef493f41ba7bf4c9b..cb85183ef073484c9cec7f3b5f2ad553a53bd992 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -11,6 +11,7 @@ doctest = false editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +picker = { path = "../picker" } project = { path = "../project" } settings = { path = "../settings" } util = { path = "../util" } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 82368788940d92b24a88bfee73b10e1380bccf74..dd19f61a15f549d9b700a1fa06031104fd29adff 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,13 +1,12 @@ -use editor::Editor; use fuzzy::PathMatch; use gpui::{ - actions, elements::*, impl_internal_actions, keymap, AppContext, Axis, Entity, ModelHandle, - MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, + actions, elements::*, impl_internal_actions, AppContext, Entity, ModelHandle, + MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, }; +use picker::{Picker, PickerDelegate}; use project::{Project, ProjectPath, WorktreeId}; use settings::Settings; use std::{ - cmp, path::Path, sync::{ atomic::{self, AtomicBool}, @@ -15,15 +14,11 @@ use std::{ }, }; use util::post_inc; -use workspace::{ - menu::{Confirm, SelectNext, SelectPrev}, - Workspace, -}; +use workspace::Workspace; pub struct FileFinder { - handle: WeakViewHandle, project: ModelHandle, - query_editor: ViewHandle, + picker: ViewHandle>, search_count: usize, latest_search_id: usize, latest_search_did_cancel: bool, @@ -31,7 +26,6 @@ pub struct FileFinder { matches: Vec, selected: Option<(usize, Arc)>, cancel_flag: Arc, - list_state: UniformListState, } #[derive(Clone)] @@ -42,10 +36,7 @@ impl_internal_actions!(file_finder, [Select]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(FileFinder::toggle); - cx.add_action(FileFinder::confirm); - cx.add_action(FileFinder::select); - cx.add_action(FileFinder::select_prev); - cx.add_action(FileFinder::select_next); + Picker::::init(cx); } pub enum Event { @@ -62,140 +53,16 @@ impl View for FileFinder { "FileFinder" } - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let settings = cx.global::(); - Align::new( - ConstrainedBox::new( - Container::new( - Flex::new(Axis::Vertical) - .with_child( - ChildView::new(&self.query_editor) - .contained() - .with_style(settings.theme.selector.input_editor.container) - .boxed(), - ) - .with_child( - FlexItem::new(self.render_matches(cx)) - .flex(1., false) - .boxed(), - ) - .boxed(), - ) - .with_style(settings.theme.selector.container) - .boxed(), - ) - .with_max_width(500.0) - .with_max_height(420.0) - .boxed(), - ) - .top() - .named("file finder") + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + ChildView::new(self.picker.clone()).boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.query_editor); - } - - fn keymap_context(&self, _: &AppContext) -> keymap::Context { - let mut cx = Self::default_keymap_context(); - cx.set.insert("menu".into()); - cx + cx.focus(&self.picker); } } impl FileFinder { - fn render_matches(&self, cx: &AppContext) -> ElementBox { - if self.matches.is_empty() { - let settings = cx.global::(); - return Container::new( - Label::new( - "No matches".into(), - settings.theme.selector.empty.label.clone(), - ) - .boxed(), - ) - .with_style(settings.theme.selector.empty.container) - .named("empty matches"); - } - - let handle = self.handle.clone(); - let list = - UniformList::new( - self.list_state.clone(), - self.matches.len(), - move |mut range, items, cx| { - let cx = cx.as_ref(); - let finder = handle.upgrade(cx).unwrap(); - let finder = finder.read(cx); - let start = range.start; - range.end = cmp::min(range.end, finder.matches.len()); - items.extend(finder.matches[range].iter().enumerate().map( - move |(i, path_match)| finder.render_match(path_match, start + i, cx), - )); - }, - ); - - Container::new(list.boxed()) - .with_margin_top(6.0) - .named("matches") - } - - fn render_match(&self, path_match: &PathMatch, index: usize, cx: &AppContext) -> ElementBox { - let selected_index = self.selected_index(); - let settings = cx.global::(); - let style = if index == selected_index { - &settings.theme.selector.active_item - } else { - &settings.theme.selector.item - }; - let (file_name, file_name_positions, full_path, full_path_positions) = - self.labels_for_match(path_match); - let container = Container::new( - Flex::row() - // .with_child( - // Container::new( - // LineBox::new( - // Svg::new("icons/file-16.svg") - // .with_color(style.label.text.color) - // .boxed(), - // style.label.text.clone(), - // ) - // .boxed(), - // ) - // .with_padding_right(6.0) - // .boxed(), - // ) - .with_child( - Flex::column() - .with_child( - Label::new(file_name.to_string(), style.label.clone()) - .with_highlights(file_name_positions) - .boxed(), - ) - .with_child( - Label::new(full_path, style.label.clone()) - .with_highlights(full_path_positions) - .boxed(), - ) - .flex(1., false) - .boxed(), - ) - .boxed(), - ) - .with_style(style.container); - - let action = Select(ProjectPath { - worktree_id: WorktreeId::from_usize(path_match.worktree_id), - path: path_match.path.clone(), - }); - EventHandler::new(container.boxed()) - .on_mouse_down(move |cx| { - cx.dispatch_action(action.clone()); - true - }) - .named("match") - } - fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec, String, Vec) { let path_string = path_match.path.to_string_lossy(); let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join(""); @@ -252,16 +119,12 @@ impl FileFinder { pub fn new(project: ModelHandle, cx: &mut ViewContext) -> Self { cx.observe(&project, Self::project_updated).detach(); - let query_editor = cx.add_view(|cx| { - Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx) - }); - cx.subscribe(&query_editor, Self::on_query_editor_event) - .detach(); + let handle = cx.weak_handle(); + let picker = cx.add_view(|cx| Picker::new(handle, cx)); Self { - handle: cx.weak_handle(), project, - query_editor, + picker, search_count: 0, latest_search_id: 0, latest_search_did_cancel: false, @@ -269,36 +132,60 @@ impl FileFinder { matches: Vec::new(), selected: None, cancel_flag: Arc::new(AtomicBool::new(false)), - list_state: Default::default(), } } fn project_updated(&mut self, _: ModelHandle, cx: &mut ViewContext) { - let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); - self.spawn_search(query, cx).detach(); + self.spawn_search(self.latest_search_query.clone(), cx) + .detach(); + } + + fn spawn_search(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { + let search_id = util::post_inc(&mut self.search_count); + self.cancel_flag.store(true, atomic::Ordering::Relaxed); + self.cancel_flag = Arc::new(AtomicBool::new(false)); + let cancel_flag = self.cancel_flag.clone(); + let project = self.project.clone(); + cx.spawn(|this, mut cx| async move { + let matches = project + .read_with(&cx, |project, cx| { + project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx) + }) + .await; + let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed); + this.update(&mut cx, |this, cx| { + this.set_matches(search_id, did_cancel, query, matches, cx) + }); + }) } - fn on_query_editor_event( + fn set_matches( &mut self, - _: ViewHandle, - event: &editor::Event, + search_id: usize, + did_cancel: bool, + query: String, + matches: Vec, cx: &mut ViewContext, ) { - match event { - editor::Event::BufferEdited { .. } => { - let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); - if query.is_empty() { - self.latest_search_id = post_inc(&mut self.search_count); - self.matches.clear(); - cx.notify(); - } else { - self.spawn_search(query, cx).detach(); - } + if search_id >= self.latest_search_id { + self.latest_search_id = search_id; + if self.latest_search_did_cancel && query == self.latest_search_query { + util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a)); + } else { + self.matches = matches; } - editor::Event::Blurred => cx.emit(Event::Dismissed), - _ => {} + self.latest_search_query = query; + self.latest_search_did_cancel = did_cancel; + cx.notify(); + self.picker.update(cx, |_, cx| cx.notify()); } } +} + +impl PickerDelegate for FileFinder { + fn match_count(&self) -> usize { + self.matches.len() + } fn selected_index(&self) -> usize { if let Some(selected) = self.selected.as_ref() { @@ -313,31 +200,24 @@ impl FileFinder { 0 } - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - let mut selected_index = self.selected_index(); - if selected_index > 0 { - selected_index -= 1; - let mat = &self.matches[selected_index]; - self.selected = Some((mat.worktree_id, mat.path.clone())); - } - self.list_state - .scroll_to(ScrollTarget::Show(selected_index)); + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + let mat = &self.matches[ix]; + self.selected = Some((mat.worktree_id, mat.path.clone())); cx.notify(); } - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - let mut selected_index = self.selected_index(); - if selected_index + 1 < self.matches.len() { - selected_index += 1; - let mat = &self.matches[selected_index]; - self.selected = Some((mat.worktree_id, mat.path.clone())); + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { + if query.is_empty() { + self.latest_search_id = post_inc(&mut self.search_count); + self.matches.clear(); + cx.notify(); + Task::ready(()) + } else { + self.spawn_search(query, cx) } - self.list_state - .scroll_to(ScrollTarget::Show(selected_index)); - cx.notify(); } - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + fn confirm(&mut self, cx: &mut ViewContext) { if let Some(m) = self.matches.get(self.selected_index()) { cx.emit(Event::Selected(ProjectPath { worktree_id: WorktreeId::from_usize(m.worktree_id), @@ -346,56 +226,57 @@ impl FileFinder { } } - fn select(&mut self, Select(project_path): &Select, cx: &mut ViewContext) { - cx.emit(Event::Selected(project_path.clone())); + fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Dismissed); } - fn spawn_search(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { - let search_id = util::post_inc(&mut self.search_count); - self.cancel_flag.store(true, atomic::Ordering::Relaxed); - self.cancel_flag = Arc::new(AtomicBool::new(false)); - let cancel_flag = self.cancel_flag.clone(); - let project = self.project.clone(); - cx.spawn(|this, mut cx| async move { - let matches = project - .read_with(&cx, |project, cx| { - project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx) - }) - .await; - let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed); - this.update(&mut cx, |this, cx| { - this.update_matches((search_id, did_cancel, query, matches), cx) - }); - }) - } + fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox { + let path_match = &self.matches[ix]; + let settings = cx.global::(); + let style = if selected { + &settings.theme.selector.active_item + } else { + &settings.theme.selector.item + }; + let (file_name, file_name_positions, full_path, full_path_positions) = + self.labels_for_match(path_match); + let action = Select(ProjectPath { + worktree_id: WorktreeId::from_usize(path_match.worktree_id), + path: path_match.path.clone(), + }); - fn update_matches( - &mut self, - (search_id, did_cancel, query, matches): (usize, bool, String, Vec), - cx: &mut ViewContext, - ) { - if search_id >= self.latest_search_id { - self.latest_search_id = search_id; - if self.latest_search_did_cancel && query == self.latest_search_query { - util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a)); - } else { - self.matches = matches; - } - self.latest_search_query = query; - self.latest_search_did_cancel = did_cancel; - self.list_state - .scroll_to(ScrollTarget::Show(self.selected_index())); - cx.notify(); - } + EventHandler::new( + Flex::column() + .with_child( + Label::new(file_name.to_string(), style.label.clone()) + .with_highlights(file_name_positions) + .boxed(), + ) + .with_child( + Label::new(full_path, style.label.clone()) + .with_highlights(full_path_positions) + .boxed(), + ) + .flex(1., false) + .contained() + .with_style(style.container) + .boxed(), + ) + .on_mouse_down(move |cx| { + cx.dispatch_action(action.clone()); + true + }) + .named("match") } } #[cfg(test)] mod tests { use super::*; - use editor::Input; + use editor::{Editor, Input}; use serde_json::json; use std::path::PathBuf; + use workspace::menu::{Confirm, SelectNext}; use workspace::{Workspace, WorkspaceParams}; #[ctor::ctor] @@ -518,25 +399,21 @@ mod tests { // Simulate a search being cancelled after the time limit, // returning only a subset of the matches that would have been found. finder.spawn_search(query.clone(), cx).detach(); - finder.update_matches( - ( - finder.latest_search_id, - true, // did-cancel - query.clone(), - vec![matches[1].clone(), matches[3].clone()], - ), + finder.set_matches( + finder.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[1].clone(), matches[3].clone()], cx, ); // Simulate another cancellation. finder.spawn_search(query.clone(), cx).detach(); - finder.update_matches( - ( - finder.latest_search_id, - true, // did-cancel - query.clone(), - vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], - ), + finder.set_matches( + finder.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], cx, ); @@ -631,9 +508,9 @@ mod tests { finder.update(cx, |f, cx| { assert_eq!(f.matches.len(), 2); assert_eq!(f.selected_index(), 0); - f.select_next(&SelectNext, cx); + f.set_selected_index(1, cx); assert_eq!(f.selected_index(), 1); - f.select_prev(&SelectPrev, cx); + f.set_selected_index(0, cx); assert_eq!(f.selected_index(), 0); }); } diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 38bcdeda307fb9f6a541fdd5667124336296e0fb..d21bff3945666e06f0a6cd41cd5b5894e99a2ea8 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -161,14 +161,18 @@ impl Picker { self.update_task = Some(cx.spawn(|this, mut cx| async move { update.await; this.update(&mut cx, |this, cx| { - cx.notify(); - this.update_task.take(); + if let Some(delegate) = this.delegate.upgrade(cx) { + let index = delegate.read(cx).selected_index(); + this.list_state.scroll_to(ScrollTarget::Show(index)); + cx.notify(); + this.update_task.take(); + } }); })); } } - fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { + pub fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { if let Some(delegate) = self.delegate.upgrade(cx) { let index = 0; delegate.update(cx, |delegate, cx| delegate.set_selected_index(0, cx)); @@ -177,7 +181,7 @@ impl Picker { } } - fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { + pub fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { if let Some(delegate) = self.delegate.upgrade(cx) { let index = delegate.update(cx, |delegate, cx| { let match_count = delegate.match_count(); @@ -190,7 +194,7 @@ impl Picker { } } - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { if let Some(delegate) = self.delegate.upgrade(cx) { let index = delegate.update(cx, |delegate, cx| { let mut selected_index = delegate.selected_index(); @@ -205,7 +209,7 @@ impl Picker { } } - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { if let Some(delegate) = self.delegate.upgrade(cx) { let index = delegate.update(cx, |delegate, cx| { let mut selected_index = delegate.selected_index(); From 20657566b3dfa3d33dad60e72ef326917857b232 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 14 Apr 2022 18:28:26 -0700 Subject: [PATCH 09/18] Make all pickers respect mouse clicks --- crates/file_finder/src/file_finder.rs | 50 +++++++++------------------ crates/picker/src/picker.rs | 26 ++++++++++++-- crates/workspace/src/menu.rs | 5 +++ 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index dd19f61a15f549d9b700a1fa06031104fd29adff..755ba3dd76061a9f3cb94c4ced2b8efe4c51cc00 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,7 +1,7 @@ use fuzzy::PathMatch; use gpui::{ - actions, elements::*, impl_internal_actions, AppContext, Entity, ModelHandle, - MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, + actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, + View, ViewContext, ViewHandle, }; use picker::{Picker, PickerDelegate}; use project::{Project, ProjectPath, WorktreeId}; @@ -28,11 +28,7 @@ pub struct FileFinder { cancel_flag: Arc, } -#[derive(Clone)] -pub struct Select(pub ProjectPath); - actions!(file_finder, [Toggle]); -impl_internal_actions!(file_finder, [Select]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(FileFinder::toggle); @@ -240,33 +236,21 @@ impl PickerDelegate for FileFinder { }; let (file_name, file_name_positions, full_path, full_path_positions) = self.labels_for_match(path_match); - let action = Select(ProjectPath { - worktree_id: WorktreeId::from_usize(path_match.worktree_id), - path: path_match.path.clone(), - }); - - EventHandler::new( - Flex::column() - .with_child( - Label::new(file_name.to_string(), style.label.clone()) - .with_highlights(file_name_positions) - .boxed(), - ) - .with_child( - Label::new(full_path, style.label.clone()) - .with_highlights(full_path_positions) - .boxed(), - ) - .flex(1., false) - .contained() - .with_style(style.container) - .boxed(), - ) - .on_mouse_down(move |cx| { - cx.dispatch_action(action.clone()); - true - }) - .named("match") + Flex::column() + .with_child( + Label::new(file_name.to_string(), style.label.clone()) + .with_highlights(file_name_positions) + .boxed(), + ) + .with_child( + Label::new(full_path, style.label.clone()) + .with_highlights(full_path_positions) + .boxed(), + ) + .flex(1., false) + .contained() + .with_style(style.container) + .named("match") } } diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index d21bff3945666e06f0a6cd41cd5b5894e99a2ea8..9feb944e3adb74e2e9f6bc06e34fc1e981340565 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -1,7 +1,7 @@ use editor::Editor; use gpui::{ elements::{ - ChildView, Flex, FlexItem, Label, ParentElement, ScrollTarget, UniformList, + ChildView, EventHandler, Flex, FlexItem, Label, ParentElement, ScrollTarget, UniformList, UniformListState, }, keymap, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task, @@ -9,7 +9,9 @@ use gpui::{ }; use settings::Settings; use std::cmp; -use workspace::menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; +use workspace::menu::{ + Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev, +}; pub struct Picker { delegate: WeakViewHandle, @@ -78,6 +80,7 @@ impl Picker { cx.add_action(Self::select_last); cx.add_action(Self::select_next); cx.add_action(Self::select_prev); + cx.add_action(Self::select_index); cx.add_action(Self::confirm); cx.add_action(Self::cancel); } @@ -126,7 +129,14 @@ impl Picker { let delegate = delegate.read(cx); let selected_ix = delegate.selected_index(); range.end = cmp::min(range.end, delegate.match_count()); - items.extend(range.map(move |ix| delegate.render_match(ix, ix == selected_ix, cx))); + items.extend(range.map(move |ix| { + EventHandler::new(delegate.render_match(ix, ix == selected_ix, cx)) + .on_mouse_down(move |cx| { + cx.dispatch_action(SelectIndex(ix)); + true + }) + .boxed() + })); }, ) .contained() @@ -181,6 +191,16 @@ impl Picker { } } + pub fn select_index(&mut self, action: &SelectIndex, cx: &mut ViewContext) { + if let Some(delegate) = self.delegate.upgrade(cx) { + let index = action.0; + delegate.update(cx, |delegate, cx| { + delegate.set_selected_index(index, cx); + delegate.confirm(cx); + }); + } + } + pub fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { if let Some(delegate) = self.delegate.upgrade(cx) { let index = delegate.update(cx, |delegate, cx| { diff --git a/crates/workspace/src/menu.rs b/crates/workspace/src/menu.rs index 81716028b970e56aabcb77b11032ba325b0aaf38..c37ad530bbfb13d0b2c9cc18e50b7f6ff62e1369 100644 --- a/crates/workspace/src/menu.rs +++ b/crates/workspace/src/menu.rs @@ -1,3 +1,6 @@ +#[derive(Clone)] +pub struct SelectIndex(pub usize); + gpui::actions!( menu, [ @@ -9,3 +12,5 @@ gpui::actions!( SelectLast ] ); + +gpui::impl_internal_actions!(menu, [SelectIndex]); From d0413ac0e1e9f7c333ab012b7f3dd7cb1408de45 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 15 Apr 2022 10:48:01 -0700 Subject: [PATCH 10/18] Fix crash when dropping a task that is awaiting a call to Background::scoped Co-authored-by: Keith Simmons --- crates/gpui/src/executor.rs | 41 +++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 2c3a8df8a1468a127589bef5ea8f12ec427b8e28..6d8e946f589a44f521b71596501b4a23ab5d59a6 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -8,7 +8,7 @@ use std::{ mem, pin::Pin, rc::Rc, - sync::Arc, + sync::{mpsc, Arc}, task::{Context, Poll}, thread, time::Duration, @@ -625,13 +625,9 @@ impl Background { where F: FnOnce(&mut Scope<'scope>), { - let mut scope = Scope { - futures: Default::default(), - _phantom: PhantomData, - }; + let mut scope = Scope::new(); (scheduler)(&mut scope); - let spawned = scope - .futures + let spawned = mem::take(&mut scope.futures) .into_iter() .map(|f| self.spawn(f)) .collect::>(); @@ -669,24 +665,53 @@ impl Background { pub struct Scope<'a> { futures: Vec + Send + 'static>>>, + tx: Option>, + rx: mpsc::Receiver<()>, _phantom: PhantomData<&'a ()>, } impl<'a> Scope<'a> { + fn new() -> Self { + let (tx, rx) = mpsc::channel(); + Self { + tx: Some(tx), + rx, + futures: Default::default(), + _phantom: PhantomData, + } + } + pub fn spawn(&mut self, f: F) where F: Future + Send + 'a, { + let tx = self.tx.clone().unwrap(); + + // Safety: The 'a lifetime is guaranteed to outlive any of these futures because + // dropping this `Scope` blocks until all of the futures have resolved. let f = unsafe { mem::transmute::< Pin + Send + 'a>>, Pin + Send + 'static>>, - >(Box::pin(f)) + >(Box::pin(async move { + f.await; + drop(tx); + })) }; self.futures.push(f); } } +impl<'a> Drop for Scope<'a> { + fn drop(&mut self) { + self.tx.take().unwrap(); + + // Wait until the channel is closed, which means that all of the spawned + // futures have resolved. + self.rx.recv().ok(); + } +} + impl Task { pub fn ready(value: T) -> Self { Self::Ready(Some(value)) From df0b5779a8c50ed071840118700fc48d0d547281 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 15 Apr 2022 10:51:26 -0700 Subject: [PATCH 11/18] Remove unused crossbeam-channel dependency in zed crate Co-authored-by: Keith Simmons --- Cargo.lock | 1 - crates/zed/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2d65dd9a5c5103f441e4b0ff5a5e2cef45a899d..72fefd90ec51e147f8566ef345d736eebf06b2a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6175,7 +6175,6 @@ dependencies = [ "collections", "command_palette", "contacts_panel", - "crossbeam-channel", "ctor", "diagnostics", "dirs 3.0.1", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 24a06b9f6744e5a8cad39e122d59f4be6198a403..7411b53441ab7d5c48c934c3c7892add8652e1c5 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -65,7 +65,6 @@ anyhow = "1.0.38" async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } async-recursion = "0.3" async-trait = "0.1" -crossbeam-channel = "0.5.0" ctor = "0.1.20" dirs = "3.0" easy-parallel = "3.1.0" From c56e2ead23128c427d7e4ac62d3755625227df69 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 15 Apr 2022 13:25:21 -0700 Subject: [PATCH 12/18] Fix hang when dropping ::scoped future under deterministic executor Co-authored-by: Nathan Sobo Co-authored-by: Keith Simmons --- crates/gpui/src/executor.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 6d8e946f589a44f521b71596501b4a23ab5d59a6..24ab663071b4c47da2468763d41994e9068ea2b2 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, Result}; use async_task::Runnable; +use futures::channel::mpsc; use smol::{channel, prelude::*, Executor}; use std::{ any::Any, @@ -8,7 +9,7 @@ use std::{ mem, pin::Pin, rc::Rc, - sync::{mpsc, Arc}, + sync::Arc, task::{Context, Poll}, thread, time::Duration, @@ -621,11 +622,11 @@ impl Background { Err(async { *future.await.downcast().unwrap() }) } - pub async fn scoped<'scope, F>(&self, scheduler: F) + pub async fn scoped<'scope, F>(self: &Arc, scheduler: F) where F: FnOnce(&mut Scope<'scope>), { - let mut scope = Scope::new(); + let mut scope = Scope::new(self.clone()); (scheduler)(&mut scope); let spawned = mem::take(&mut scope.futures) .into_iter() @@ -664,6 +665,7 @@ impl Background { } pub struct Scope<'a> { + executor: Arc, futures: Vec + Send + 'static>>>, tx: Option>, rx: mpsc::Receiver<()>, @@ -671,9 +673,10 @@ pub struct Scope<'a> { } impl<'a> Scope<'a> { - fn new() -> Self { - let (tx, rx) = mpsc::channel(); + fn new(executor: Arc) -> Self { + let (tx, rx) = mpsc::channel(1); Self { + executor, tx: Some(tx), rx, futures: Default::default(), @@ -708,7 +711,7 @@ impl<'a> Drop for Scope<'a> { // Wait until the channel is closed, which means that all of the spawned // futures have resolved. - self.rx.recv().ok(); + self.executor.block(self.rx.next()); } } From 3bbc021a7eee040e415923a6ac2211df3f7e32c2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 15 Apr 2022 14:28:01 -0700 Subject: [PATCH 13/18] Use Picker in Outline view --- Cargo.lock | 1 + assets/keymaps/default.json | 12 - crates/file_finder/src/file_finder.rs | 7 +- crates/outline/Cargo.toml | 1 + crates/outline/src/outline.rs | 224 +++++------------- crates/picker/src/picker.rs | 112 +++++---- crates/project_symbols/src/project_symbols.rs | 9 +- 7 files changed, 130 insertions(+), 236 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 72fefd90ec51e147f8566ef345d736eebf06b2a0..0a4540ba39db3a6110d5bfcbb749de3bd201bc2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3374,6 +3374,7 @@ dependencies = [ "gpui", "language", "ordered-float", + "picker", "postage", "settings", "smol", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index e517f853d5c66f70c2c811885c08e091bc9f946b..506dd362d4c3ec8262ac472aa0321951a3a96ef3 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -250,22 +250,10 @@ "\n" ] }, - "OutlineView": { - "escape": "outline::Toggle" - }, - "ProjectSymbolsView": { - "escape": "project_symbols::Toggle" - }, - "ThemeSelector": { - "escape": "theme_selector::Toggle" - }, "GoToLine": { "escape": "go_to_line::Toggle", "enter": "go_to_line::Confirm" }, - "FileFinder": { - "escape": "file_finder::Toggle" - }, "ChatPanel": { "enter": "chat_panel::Send" }, diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 755ba3dd76061a9f3cb94c4ced2b8efe4c51cc00..9877cef3d80e8d4fa47fb1ddbbf4059ef1e93b46 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -113,14 +113,11 @@ impl FileFinder { } pub fn new(project: ModelHandle, cx: &mut ViewContext) -> Self { - cx.observe(&project, Self::project_updated).detach(); - let handle = cx.weak_handle(); - let picker = cx.add_view(|cx| Picker::new(handle, cx)); - + cx.observe(&project, Self::project_updated).detach(); Self { project, - picker, + picker: cx.add_view(|cx| Picker::new(handle, cx)), search_count: 0, latest_search_id: 0, latest_search_did_cancel: false, diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index fc9444a14c705b9fdd6aeb5a1bb1b8b0f6e6c851..5b4751e620485b81931099cd980d4e2a35524d92 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -12,6 +12,7 @@ editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } language = { path = "../language" } +picker = { path = "../picker" } settings = { path = "../settings" } text = { path = "../text" } workspace = { path = "../workspace" } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 08bc18a016a62f72d3aaf4b60c7aff33bb4cbfde..a5492a8f69214e7447485d2442871ed8b5c3d33d 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -4,38 +4,31 @@ use editor::{ }; use fuzzy::StringMatch; use gpui::{ - actions, elements::*, geometry::vector::Vector2F, keymap, AppContext, Axis, Entity, - MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle, + actions, elements::*, geometry::vector::Vector2F, AppContext, Entity, MutableAppContext, + RenderContext, Task, View, ViewContext, ViewHandle, }; use language::Outline; use ordered_float::OrderedFloat; +use picker::{Picker, PickerDelegate}; use settings::Settings; use std::cmp::{self, Reverse}; -use workspace::{ - menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}, - Workspace, -}; +use workspace::Workspace; actions!(outline, [Toggle]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(OutlineView::toggle); - cx.add_action(OutlineView::confirm); - cx.add_action(OutlineView::select_prev); - cx.add_action(OutlineView::select_next); - cx.add_action(OutlineView::select_first); - cx.add_action(OutlineView::select_last); + Picker::::init(cx); } struct OutlineView { - handle: WeakViewHandle, + picker: ViewHandle>, active_editor: ViewHandle, outline: Outline, selected_match_index: usize, prev_scroll_position: Option, matches: Vec, - query_editor: ViewHandle, - list_state: UniformListState, + last_query: String, } pub enum Event { @@ -55,38 +48,12 @@ impl View for OutlineView { "OutlineView" } - fn keymap_context(&self, _: &AppContext) -> keymap::Context { - let mut cx = Self::default_keymap_context(); - cx.set.insert("menu".into()); - cx - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let settings = cx.global::(); - - Flex::new(Axis::Vertical) - .with_child( - Container::new(ChildView::new(&self.query_editor).boxed()) - .with_style(settings.theme.selector.input_editor.container) - .boxed(), - ) - .with_child( - FlexItem::new(self.render_matches(cx)) - .flex(1.0, false) - .boxed(), - ) - .contained() - .with_style(settings.theme.selector.container) - .constrained() - .with_max_width(800.0) - .with_max_height(1200.0) - .aligned() - .top() - .named("outline view") + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + ChildView::new(self.picker.clone()).boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.query_editor); + cx.focus(&self.picker); } } @@ -96,24 +63,16 @@ impl OutlineView { editor: ViewHandle, cx: &mut ViewContext, ) -> Self { - let query_editor = cx.add_view(|cx| { - Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx) - }); - cx.subscribe(&query_editor, Self::on_query_editor_event) - .detach(); - - let mut this = Self { - handle: cx.weak_handle(), + let handle = cx.weak_handle(); + Self { + picker: cx.add_view(|cx| Picker::new(handle, cx).with_max_size(800., 1200.)), + last_query: Default::default(), matches: Default::default(), selected_match_index: 0, prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))), active_editor: editor, outline, - query_editor, - list_state: Default::default(), - }; - this.update_matches(cx); - this + } } fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { @@ -137,34 +96,18 @@ impl OutlineView { } } - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if self.selected_match_index > 0 { - self.select(self.selected_match_index - 1, true, false, cx); - } - } - - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - if self.selected_match_index + 1 < self.matches.len() { - self.select(self.selected_match_index + 1, true, false, cx); - } - } - - fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { - self.select(0, true, false, cx); - } - - fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { - self.select(self.matches.len().saturating_sub(1), true, false, cx); + fn restore_active_editor(&mut self, cx: &mut MutableAppContext) { + self.active_editor.update(cx, |editor, cx| { + editor.highlight_rows(None); + if let Some(scroll_position) = self.prev_scroll_position { + editor.set_scroll_position(scroll_position, cx); + } + }) } - fn select(&mut self, index: usize, navigate: bool, center: bool, cx: &mut ViewContext) { - self.selected_match_index = index; - self.list_state.scroll_to(if center { - ScrollTarget::Center(index) - } else { - ScrollTarget::Show(index) - }); - if navigate { + fn set_selected_index(&mut self, ix: usize, navigate: bool, cx: &mut ViewContext) { + self.selected_match_index = ix; + if navigate && !self.matches.is_empty() { let selected_match = &self.matches[self.selected_match_index]; let outline_item = &self.outline.items[selected_match.candidate_id]; self.active_editor.update(cx, |active_editor, cx| { @@ -181,27 +124,6 @@ impl OutlineView { cx.notify(); } - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - self.prev_scroll_position.take(); - self.active_editor.update(cx, |active_editor, cx| { - if let Some(rows) = active_editor.highlighted_rows() { - let snapshot = active_editor.snapshot(cx).display_snapshot; - let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot); - active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx); - } - }); - cx.emit(Event::Dismissed); - } - - fn restore_active_editor(&mut self, cx: &mut MutableAppContext) { - self.active_editor.update(cx, |editor, cx| { - editor.highlight_rows(None); - if let Some(scroll_position) = self.prev_scroll_position { - editor.set_scroll_position(scroll_position, cx); - } - }) - } - fn on_event( workspace: &mut Workspace, _: ViewHandle, @@ -212,24 +134,27 @@ impl OutlineView { Event::Dismissed => workspace.dismiss_modal(cx), } } +} - fn on_query_editor_event( - &mut self, - _: ViewHandle, - event: &editor::Event, - cx: &mut ViewContext, - ) { - match event { - editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::BufferEdited { .. } => self.update_matches(cx), - _ => {} - } +impl PickerDelegate for OutlineView { + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_match_index } - fn update_matches(&mut self, cx: &mut ViewContext) { + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + self.set_selected_index(ix, true, cx); + } + + fn center_selection_after_match_updates(&self) -> bool { + true + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { let selected_index; - let navigate_to_selected_index; - let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); if query.is_empty() { self.restore_active_editor(cx); self.matches = self @@ -271,7 +196,6 @@ impl OutlineView { .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance))) .map(|(ix, _, _)| ix) .unwrap_or(0); - navigate_to_selected_index = false; } else { self.matches = smol::block_on(self.outline.search(&query, cx.background().clone())); selected_index = self @@ -281,57 +205,33 @@ impl OutlineView { .max_by_key(|(_, m)| OrderedFloat(m.score)) .map(|(ix, _)| ix) .unwrap_or(0); - navigate_to_selected_index = !self.matches.is_empty(); } - self.select(selected_index, navigate_to_selected_index, true, cx); + self.last_query = query; + self.set_selected_index(selected_index, false, cx); + Task::ready(()) } - fn render_matches(&self, cx: &AppContext) -> ElementBox { - if self.matches.is_empty() { - let settings = cx.global::(); - return Container::new( - Label::new( - "No matches".into(), - settings.theme.selector.empty.label.clone(), - ) - .boxed(), - ) - .with_style(settings.theme.selector.empty.container) - .named("empty matches"); - } - - let handle = self.handle.clone(); - let list = UniformList::new( - self.list_state.clone(), - self.matches.len(), - move |mut range, items, cx| { - let cx = cx.as_ref(); - let view = handle.upgrade(cx).unwrap(); - let view = view.read(cx); - let start = range.start; - range.end = cmp::min(range.end, view.matches.len()); - items.extend( - view.matches[range] - .iter() - .enumerate() - .map(move |(ix, m)| view.render_match(m, start + ix, cx)), - ); - }, - ); + fn confirm(&mut self, cx: &mut ViewContext) { + self.prev_scroll_position.take(); + self.active_editor.update(cx, |active_editor, cx| { + if let Some(rows) = active_editor.highlighted_rows() { + let snapshot = active_editor.snapshot(cx).display_snapshot; + let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot); + active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx); + } + }); + cx.emit(Event::Dismissed); + } - Container::new(list.boxed()) - .with_margin_top(6.0) - .named("matches") + fn dismiss(&mut self, cx: &mut ViewContext) { + self.restore_active_editor(cx); + cx.emit(Event::Dismissed); } - fn render_match( - &self, - string_match: &StringMatch, - index: usize, - cx: &AppContext, - ) -> ElementBox { + fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox { let settings = cx.global::(); - let style = if index == self.selected_match_index { + let string_match = &self.matches[ix]; + let style = if selected { &settings.theme.selector.active_item } else { &settings.theme.selector.item diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 9feb944e3adb74e2e9f6bc06e34fc1e981340565..76b9d99d29e1f2c679e091140a0c33db2e2791c2 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -1,9 +1,10 @@ use editor::Editor; use gpui::{ elements::{ - ChildView, EventHandler, Flex, FlexItem, Label, ParentElement, ScrollTarget, UniformList, + ChildView, EventHandler, Flex, Label, ParentElement, ScrollTarget, UniformList, UniformListState, }, + geometry::vector::{vec2f, Vector2F}, keymap, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; @@ -18,6 +19,7 @@ pub struct Picker { query_editor: ViewHandle, list_state: UniformListState, update_task: Option>, + max_size: Vector2F, } pub trait PickerDelegate: View { @@ -28,6 +30,9 @@ pub trait PickerDelegate: View { fn confirm(&mut self, cx: &mut ViewContext); fn dismiss(&mut self, cx: &mut ViewContext); fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox; + fn center_selection_after_match_updates(&self) -> bool { + false + } } impl Entity for Picker { @@ -41,6 +46,13 @@ impl View for Picker { fn render(&mut self, cx: &mut RenderContext) -> gpui::ElementBox { let settings = cx.global::(); + let delegate = self.delegate.clone(); + let match_count = if let Some(delegate) = delegate.upgrade(cx.app) { + delegate.read(cx).match_count() + } else { + 0 + }; + Flex::new(Axis::Vertical) .with_child( ChildView::new(&self.query_editor) @@ -49,15 +61,44 @@ impl View for Picker { .boxed(), ) .with_child( - FlexItem::new(self.render_matches(cx)) - .flex(1., false) - .boxed(), + if match_count == 0 { + Label::new( + "No matches".into(), + settings.theme.selector.empty.label.clone(), + ) + .contained() + .with_style(settings.theme.selector.empty.container) + } else { + UniformList::new( + self.list_state.clone(), + match_count, + move |mut range, items, cx| { + let cx = cx.as_ref(); + let delegate = delegate.upgrade(cx).unwrap(); + let delegate = delegate.read(cx); + let selected_ix = delegate.selected_index(); + range.end = cmp::min(range.end, delegate.match_count()); + items.extend(range.map(move |ix| { + EventHandler::new(delegate.render_match(ix, ix == selected_ix, cx)) + .on_mouse_down(move |cx| { + cx.dispatch_action(SelectIndex(ix)); + true + }) + .boxed() + })); + }, + ) + .contained() + .with_margin_top(6.0) + } + .flex(1., false) + .boxed(), ) .contained() .with_style(settings.theme.selector.container) .constrained() - .with_max_width(500.0) - .with_max_height(420.0) + .with_max_width(self.max_size.x()) + .with_max_height(self.max_size.y()) .aligned() .top() .named("picker") @@ -91,57 +132,20 @@ impl Picker { }); cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); - let mut this = Self { + let this = Self { query_editor, list_state: Default::default(), update_task: None, delegate, + max_size: vec2f(500., 420.), }; - this.update_matches(cx); + cx.defer(|this, cx| this.update_matches(cx)); this } - fn render_matches(&self, cx: &AppContext) -> ElementBox { - let delegate = self.delegate.clone(); - let match_count = if let Some(delegate) = delegate.upgrade(cx) { - delegate.read(cx).match_count() - } else { - 0 - }; - - if match_count == 0 { - let settings = cx.global::(); - return Label::new( - "No matches".into(), - settings.theme.selector.empty.label.clone(), - ) - .contained() - .with_style(settings.theme.selector.empty.container) - .named("empty matches"); - } - - UniformList::new( - self.list_state.clone(), - match_count, - move |mut range, items, cx| { - let cx = cx.as_ref(); - let delegate = delegate.upgrade(cx).unwrap(); - let delegate = delegate.read(cx); - let selected_ix = delegate.selected_index(); - range.end = cmp::min(range.end, delegate.match_count()); - items.extend(range.map(move |ix| { - EventHandler::new(delegate.render_match(ix, ix == selected_ix, cx)) - .on_mouse_down(move |cx| { - cx.dispatch_action(SelectIndex(ix)); - true - }) - .boxed() - })); - }, - ) - .contained() - .with_margin_top(6.0) - .named("matches") + pub fn with_max_size(mut self, width: f32, height: f32) -> Self { + self.max_size = vec2f(width, height); + self } fn on_query_editor_event( @@ -172,8 +176,14 @@ impl Picker { update.await; this.update(&mut cx, |this, cx| { if let Some(delegate) = this.delegate.upgrade(cx) { - let index = delegate.read(cx).selected_index(); - this.list_state.scroll_to(ScrollTarget::Show(index)); + let delegate = delegate.read(cx); + let index = delegate.selected_index(); + let target = if delegate.center_selection_after_match_updates() { + ScrollTarget::Center(index) + } else { + ScrollTarget::Show(index) + }; + this.list_state.scroll_to(target); cx.notify(); this.update_task.take(); } diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 2c048c4b7e0c6c66c63966712433c6e7e752c14a..8e89951a00d913f079b62e4c4285cf72d778b575 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -57,18 +57,15 @@ impl View for ProjectSymbolsView { impl ProjectSymbolsView { fn new(project: ModelHandle, cx: &mut ViewContext) -> Self { let handle = cx.weak_handle(); - let picker = cx.add_view(|cx| Picker::new(handle, cx)); - let mut this = Self { - picker, + Self { project, + picker: cx.add_view(|cx| Picker::new(handle, cx)), selected_match_index: 0, symbols: Default::default(), match_candidates: Default::default(), matches: Default::default(), show_worktree_root_name: false, - }; - this.update_matches(String::new(), cx).detach(); - this + } } fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { From f5377c2f50de9d982f2f4c9397794cba898e336d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 18 Apr 2022 09:00:47 -0700 Subject: [PATCH 14/18] Highlight fuzzy match positions in command palette --- crates/command_palette/src/command_palette.rs | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index ddefe4d8308ce114ac5b0f5da78626b2b1c5d58c..0786d9b764175579b20c96d9ae56273f242dc121 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -135,15 +135,28 @@ impl PickerDelegate for CommandPalette { }) .collect::>(); cx.spawn(move |this, mut cx| async move { - let matches = fuzzy::match_strings( - &candidates, - &query, - true, - 10000, - &Default::default(), - cx.background(), - ) - .await; + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + fuzzy::match_strings( + &candidates, + &query, + true, + 10000, + &Default::default(), + cx.background(), + ) + .await + }; this.update(&mut cx, |this, _| { this.matches = matches; if this.matches.is_empty() { @@ -186,7 +199,11 @@ impl PickerDelegate for CommandPalette { let keystroke_spacing = theme.command_palette.keystroke_spacing; Flex::row() - .with_child(Label::new(mat.string.clone(), style.label.clone()).boxed()) + .with_child( + Label::new(mat.string.clone(), style.label.clone()) + .with_highlights(mat.positions.clone()) + .boxed(), + ) .with_children(command.keystrokes.iter().map(|keystroke| { Flex::row() .with_children( From 11eba96cb868d86a966d06822c51748fc62f19d8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 18 Apr 2022 11:33:15 -0700 Subject: [PATCH 15/18] Allow toggling other modals from the command palette --- crates/command_palette/src/command_palette.rs | 31 ++++++++++++++----- crates/picker/src/picker.rs | 6 +++- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 0786d9b764175579b20c96d9ae56273f242dc121..211ecdd2d03b6576527864fea5f97746e3cf92c3 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -27,6 +27,11 @@ pub struct CommandPalette { pub enum Event { Dismissed, + Confirmed { + window_id: usize, + focused_view_id: usize, + action: Box, + }, } struct Command { @@ -81,8 +86,18 @@ impl CommandPalette { cx: &mut ViewContext, ) { match event { - Event::Dismissed => { + Event::Dismissed => workspace.dismiss_modal(cx), + Event::Confirmed { + window_id, + focused_view_id, + action, + } => { + let window_id = *window_id; + let focused_view_id = *focused_view_id; + let action = (*action).boxed_clone(); workspace.dismiss_modal(cx); + cx.as_mut() + .defer(move |cx| cx.dispatch_action_at(window_id, focused_view_id, &*action)) } } } @@ -174,15 +189,15 @@ impl PickerDelegate for CommandPalette { fn confirm(&mut self, cx: &mut ViewContext) { if !self.matches.is_empty() { - let window_id = cx.window_id(); let action_ix = self.matches[self.selected_ix].candidate_id; - cx.dispatch_action_at( - window_id, - self.focused_view_id, - self.actions[action_ix].action.as_ref(), - ) + cx.emit(Event::Confirmed { + window_id: cx.window_id(), + focused_view_id: self.focused_view_id, + action: self.actions.remove(action_ix).action, + }); + } else { + cx.emit(Event::Dismissed); } - cx.emit(Event::Dismissed); } fn render_match(&self, ix: usize, selected: bool, cx: &gpui::AppContext) -> gpui::ElementBox { diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 76b9d99d29e1f2c679e091140a0c33db2e2791c2..f23467247820bf4117a8f5606dec9a009489fb61 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -20,6 +20,7 @@ pub struct Picker { list_state: UniformListState, update_task: Option>, max_size: Vector2F, + confirmed: bool, } pub trait PickerDelegate: View { @@ -138,6 +139,7 @@ impl Picker { update_task: None, delegate, max_size: vec2f(500., 420.), + confirmed: false, }; cx.defer(|this, cx| this.update_matches(cx)); this @@ -156,7 +158,7 @@ impl Picker { ) { match event { editor::Event::BufferEdited { .. } => self.update_matches(cx), - editor::Event::Blurred => { + editor::Event::Blurred if !self.confirmed => { if let Some(delegate) = self.delegate.upgrade(cx) { delegate.update(cx, |delegate, cx| { delegate.dismiss(cx); @@ -204,6 +206,7 @@ impl Picker { pub fn select_index(&mut self, action: &SelectIndex, cx: &mut ViewContext) { if let Some(delegate) = self.delegate.upgrade(cx) { let index = action.0; + self.confirmed = true; delegate.update(cx, |delegate, cx| { delegate.set_selected_index(index, cx); delegate.confirm(cx); @@ -256,6 +259,7 @@ impl Picker { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some(delegate) = self.delegate.upgrade(cx) { + self.confirmed = true; delegate.update(cx, |delegate, cx| delegate.confirm(cx)); } } From a4f259066bc11d67a0afd940cdfbe2458c606e60 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 18 Apr 2022 17:30:17 -0700 Subject: [PATCH 16/18] tmp --- crates/command_palette/src/command_palette.rs | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 211ecdd2d03b6576527864fea5f97746e3cf92c3..2f231cabb6e52840deb6c58ab52f8bce7754bafc 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -35,7 +35,7 @@ pub enum Event { } struct Command { - name: &'static str, + name: String, action: Box, keystrokes: Vec, } @@ -46,7 +46,7 @@ impl CommandPalette { let actions = cx .available_actions(cx.window_id(), focused_view_id) .map(|(name, action, bindings)| Command { - name, + name: humanize(name), action, keystrokes: bindings .last() @@ -259,6 +259,30 @@ impl PickerDelegate for CommandPalette { } } +fn humanize(name: &str) -> String { + let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count(); + let mut result = String::with_capacity(capacity); + let mut prev_char = '\0'; + for char in name.chars() { + if char == ':' { + if prev_char == ':' { + result.push(' '); + } else { + result.push(':'); + } + } else if char.is_uppercase() { + if prev_char.is_lowercase() { + result.push(' '); + } + result.push(char); + } else { + result.push(char); + } + prev_char = char; + } + result +} + impl std::fmt::Debug for Command { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Command") @@ -267,3 +291,5 @@ impl std::fmt::Debug for Command { .finish() } } + +// #[cfg(test)] From d8ad0d8e4e8459a4805ab820a1f05375d0a37acc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 18 Apr 2022 18:07:17 -0700 Subject: [PATCH 17/18] Format action names more readably in command palette Also add a unit test for the command palette --- crates/command_palette/Cargo.toml | 1 + crates/command_palette/src/command_palette.rs | 83 +++++++++++++++++-- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index ecff82f6f44a455bc1d58d45c120312e37af31a5..aeaffa3e6f0fd77082a1e4b9cd7896da9b713669 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -19,6 +19,7 @@ workspace = { path = "../workspace" } [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } workspace = { path = "../workspace", features = ["test-support"] } ctor = "0.1" diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 2f231cabb6e52840deb6c58ab52f8bce7754bafc..b535450d12eb86903c4a381a2b70a814de04a565 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -46,7 +46,7 @@ impl CommandPalette { let actions = cx .available_actions(cx.window_id(), focused_view_id) .map(|(name, action, bindings)| Command { - name: humanize(name), + name: humanize_action_name(name), action, keystrokes: bindings .last() @@ -259,26 +259,24 @@ impl PickerDelegate for CommandPalette { } } -fn humanize(name: &str) -> String { +fn humanize_action_name(name: &str) -> String { let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count(); let mut result = String::with_capacity(capacity); - let mut prev_char = '\0'; for char in name.chars() { if char == ':' { - if prev_char == ':' { + if result.ends_with(':') { result.push(' '); } else { result.push(':'); } } else if char.is_uppercase() { - if prev_char.is_lowercase() { + if !result.ends_with(' ') { result.push(' '); } - result.push(char); + result.extend(char.to_lowercase()); } else { result.push(char); } - prev_char = char; } result } @@ -292,4 +290,73 @@ impl std::fmt::Debug for Command { } } -// #[cfg(test)] +#[cfg(test)] +mod tests { + use super::*; + use editor::Editor; + use gpui::TestAppContext; + use workspace::{Workspace, WorkspaceParams}; + + #[test] + fn test_humanize_action_name() { + assert_eq!( + &humanize_action_name("editor::GoToDefinition"), + "editor: go to definition" + ); + assert_eq!( + &humanize_action_name("editor::Backspace"), + "editor: backspace" + ); + } + + #[gpui::test] + async fn test_command_palette(cx: &mut TestAppContext) { + let params = cx.update(WorkspaceParams::test); + + cx.update(|cx| { + editor::init(cx); + workspace::init(¶ms.client, cx); + init(cx); + }); + + let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); + let editor = cx.add_view(window_id, |cx| { + let mut editor = Editor::single_line(None, cx); + editor.set_text("abc", cx); + editor + }); + + workspace.update(cx, |workspace, cx| { + cx.focus(editor.clone()); + workspace.add_item(Box::new(editor.clone()), cx) + }); + + workspace.update(cx, |workspace, cx| { + CommandPalette::toggle(workspace, &Toggle, cx) + }); + + let palette = workspace.read_with(cx, |workspace, _| { + workspace + .modal() + .unwrap() + .clone() + .downcast::() + .unwrap() + }); + + palette + .update(cx, |palette, cx| { + palette.update_matches("bcksp".to_string(), cx) + }) + .await; + + palette.update(cx, |palette, cx| { + assert_eq!(palette.matches[0].string, "editor: backspace"); + palette.confirm(cx); + }); + + editor.read_with(cx, |editor, cx| { + assert_eq!(editor.text(cx), "ab"); + }); + } +} From 0af129d50a594fc61985a0a3418e304747479ea5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 18 Apr 2022 18:14:45 -0700 Subject: [PATCH 18/18] Use primary text color for keystrokes in command palette --- assets/themes/dark.json | 2 +- assets/themes/light.json | 2 +- styles/src/styleTree/commandPalette.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/themes/dark.json b/assets/themes/dark.json index e8a082d2caa021f5fc61ccd455e0facc3a4cd719..85822e8e05ec35f47a0f08bf42a53ed8db401efd 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -713,7 +713,7 @@ "key": { "text": { "family": "Zed Mono", - "color": "#9c9c9c", + "color": "#f1f1f1", "size": 12 }, "corner_radius": 3, diff --git a/assets/themes/light.json b/assets/themes/light.json index bf9b287a379d7e75dfd0d08aadaa1cc9ea08a15d..9a93749bfb3e4fea1d158ea1a5118af88ba0531b 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -713,7 +713,7 @@ "key": { "text": { "family": "Zed Mono", - "color": "#474747", + "color": "#2b2b2b", "size": 12 }, "corner_radius": 3, diff --git a/styles/src/styleTree/commandPalette.ts b/styles/src/styleTree/commandPalette.ts index f7a0f51f6eb2949914cbda235a571e549c6e8420..00005d619796be416d32407d976571d267c26d77 100644 --- a/styles/src/styleTree/commandPalette.ts +++ b/styles/src/styleTree/commandPalette.ts @@ -5,7 +5,7 @@ export default function commandPalette(theme: Theme) { return { keystrokeSpacing: 8, key: { - text: text(theme, "mono", "secondary", { size: "xs" }), + text: text(theme, "mono", "primary", { size: "xs" }), cornerRadius: 3, background: backgroundColor(theme, "info", "base"), border: border(theme, "info"),