Start working on command_palette2

Piotr Osiewicz created

Change summary

Cargo.lock                                     |  34 +
Cargo.toml                                     |   4 
assets/keymaps/default.json                    |   3 
crates/command_palette2/Cargo.toml             |  34 +
crates/command_palette2/src/command_palette.rs | 542 ++++++++++++++++++++
crates/gpui2/src/action.rs                     |   7 
crates/workspace2/src/workspace2.rs            | 301 +++++-----
crates/zed2/Cargo.toml                         |   4 
crates/zed2/src/main.rs                        |   4 
crates/zed_actions2/Cargo.toml                 |  11 
crates/zed_actions2/src/lib.rs                 |  34 +
11 files changed, 822 insertions(+), 156 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1880,6 +1880,30 @@ dependencies = [
  "zed-actions",
 ]
 
+[[package]]
+name = "command_palette2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "collections",
+ "ctor",
+ "editor2",
+ "env_logger 0.9.3",
+ "fuzzy2",
+ "gpui2",
+ "language2",
+ "picker2",
+ "project2",
+ "serde",
+ "serde_json",
+ "settings2",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+ "zed_actions2",
+]
+
 [[package]]
 name = "component_test"
 version = "0.1.0"
@@ -11362,6 +11386,7 @@ dependencies = [
  "cli",
  "client2",
  "collections",
+ "command_palette2",
  "copilot2",
  "ctor",
  "db2",
@@ -11448,6 +11473,15 @@ dependencies = [
  "util",
  "uuid 1.4.1",
  "workspace2",
+ "zed_actions2",
+]
+
+[[package]]
+name = "zed_actions2"
+version = "0.1.0"
+dependencies = [
+ "gpui2",
+ "serde",
 ]
 
 [[package]]

Cargo.toml 🔗

@@ -20,6 +20,7 @@ members = [
     "crates/collab_ui",
     "crates/collections",
     "crates/command_palette",
+    "crates/command_palette2",
     "crates/component_test",
     "crates/context_menu",
     "crates/copilot",
@@ -110,7 +111,8 @@ members = [
     "crates/xtask",
     "crates/zed",
     "crates/zed2",
-    "crates/zed-actions"
+    "crates/zed-actions",
+    "crates/zed_actions2"
 ]
 default-members = ["crates/zed"]
 resolver = "2"

assets/keymaps/default.json 🔗

@@ -387,7 +387,8 @@
     }
   },
   {
-    "context": "Workspace",
+    // todo!() fix context
+    // "context": "Workspace",
     "bindings": {
       "cmd-1": ["workspace::ActivatePane", 0],
       "cmd-2": ["workspace::ActivatePane", 1],

crates/command_palette2/Cargo.toml 🔗

@@ -0,0 +1,34 @@
+[package]
+name = "command_palette2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/command_palette.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+editor = { package = "editor2", path = "../editor2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+picker = { package = "picker2", path = "../picker2" }
+project = { package = "project2", path = "../project2" }
+settings = { package = "settings2", path = "../settings2" }
+ui = { package = "ui2", path = "../ui2" }
+util = { path = "../util" }
+theme = { package = "theme2", path = "../theme2" }
+workspace = { package="workspace2", path = "../workspace2" }
+zed_actions = { package = "zed_actions2", path = "../zed_actions2" }
+anyhow.workspace = true
+serde.workspace = true
+[dev-dependencies]
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+language = { package="language2", path = "../language2", features = ["test-support"] }
+project = { package="project2", path = "../project2", features = ["test-support"] }
+serde_json.workspace = true
+workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] }
+ctor.workspace = true
+env_logger.workspace = true

crates/command_palette2/src/command_palette.rs 🔗

@@ -0,0 +1,542 @@
+use anyhow::anyhow;
+use collections::{CommandPaletteFilter, HashMap};
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{
+    actions, div, Action, AnyElement, AnyWindowHandle, AppContext, BorrowWindow, Div, Element,
+    EventEmitter, FocusHandle, Keystroke, ParentElement, Render, View, ViewContext, VisualContext,
+    WeakView,
+};
+use picker::{Picker, PickerDelegate};
+use std::cmp::{self, Reverse};
+use ui::modal;
+use util::{
+    channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
+    ResultExt,
+};
+use workspace::{ModalEvent, Workspace};
+use zed_actions::OpenZedURL;
+
+actions!(Toggle);
+
+pub fn init(cx: &mut AppContext) {
+    dbg!("init");
+    cx.set_global(HitCounts::default());
+
+    cx.observe_new_views(
+        |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
+            dbg!("new workspace found");
+            workspace
+                .modal_layer()
+                .register_modal(Toggle, |workspace, cx| {
+                    dbg!("hitting cmd-shift-p");
+                    let Some(focus_handle) = cx.focused() else {
+                        return None;
+                    };
+
+                    Some(cx.build_view(|cx| {
+                        let delegate =
+                            CommandPaletteDelegate::new(cx.view().downgrade(), focus_handle);
+                        CommandPalette::new(delegate, cx)
+                    }))
+                });
+        },
+    )
+    .detach();
+}
+
+pub struct CommandPalette {
+    picker: View<Picker<CommandPaletteDelegate>>,
+}
+
+impl CommandPalette {
+    fn new(delegate: CommandPaletteDelegate, cx: &mut ViewContext<Self>) -> Self {
+        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+        Self { picker }
+    }
+}
+impl EventEmitter<ModalEvent> for CommandPalette {}
+
+impl Render for CommandPalette {
+    type Element = Div<Self>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        dbg!("Rendering");
+        modal(cx).w_96().child(self.picker.clone())
+    }
+}
+
+pub type CommandPaletteInterceptor =
+    Box<dyn Fn(&str, &AppContext) -> Option<CommandInterceptResult>>;
+
+pub struct CommandInterceptResult {
+    pub action: Box<dyn Action>,
+    pub string: String,
+    pub positions: Vec<usize>,
+}
+
+pub struct CommandPaletteDelegate {
+    command_palette: WeakView<CommandPalette>,
+    actions: Vec<Command>,
+    matches: Vec<StringMatch>,
+    selected_ix: usize,
+    focus_handle: FocusHandle,
+}
+
+pub enum Event {
+    Dismissed,
+    Confirmed {
+        window: AnyWindowHandle,
+        focused_view_id: usize,
+        action: Box<dyn Action>,
+    },
+}
+
+struct Command {
+    name: String,
+    action: Box<dyn Action>,
+    keystrokes: Vec<Keystroke>,
+}
+
+impl Clone for Command {
+    fn clone(&self) -> Self {
+        Self {
+            name: self.name.clone(),
+            action: self.action.boxed_clone(),
+            keystrokes: self.keystrokes.clone(),
+        }
+    }
+}
+/// Hit count for each command in the palette.
+/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because
+/// if an user already knows a keystroke for a command, they are unlikely to use a command palette to look for it.
+#[derive(Default)]
+struct HitCounts(HashMap<String, usize>);
+
+impl CommandPaletteDelegate {
+    pub fn new(command_palette: WeakView<CommandPalette>, focus_handle: FocusHandle) -> Self {
+        Self {
+            command_palette,
+            actions: Default::default(),
+            matches: vec![StringMatch {
+                candidate_id: 0,
+                score: 0.,
+                positions: vec![],
+                string: "Foo my bar".into(),
+            }],
+            selected_ix: 0,
+            focus_handle,
+        }
+    }
+}
+
+impl PickerDelegate for CommandPaletteDelegate {
+    type ListItem = Div<Picker<Self>>;
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_ix
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+        self.selected_ix = ix;
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> gpui::Task<()> {
+        let view_id = &self.focus_handle;
+        let window = cx.window();
+        cx.spawn(move |picker, mut cx| async move {
+            let mut actions = picker
+                .update(&mut cx, |this, _| this.delegate.actions.clone())
+                .expect("todo: handle picker no longer being around");
+            // _ = window
+            //     .available_actions(view_id, &cx)
+            //     .into_iter()
+            //     .flatten()
+            //     .filter_map(|(name, action, bindings)| {
+            //         let filtered = cx.read(|cx| {
+            //             if cx.has_global::<CommandPaletteFilter>() {
+            //                 let filter = cx.global::<CommandPaletteFilter>();
+            //                 filter.filtered_namespaces.contains(action.namespace())
+            //             } else {
+            //                 false
+            //             }
+            //         });
+
+            //         if filtered {
+            //             None
+            //         } else {
+            //             Some(Command {
+            //                 name: humanize_action_name(name),
+            //                 action,
+            //                 keystrokes: bindings
+            //                     .iter()
+            //                     .map(|binding| binding.keystrokes())
+            //                     .last()
+            //                     .map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
+            //             })
+            //         }
+            //     })
+            //     .collect::<Vec<_>>();
+
+            cx.read_global::<HitCounts, _>(|hit_counts, _| {
+                actions.sort_by_key(|action| {
+                    (
+                        Reverse(hit_counts.0.get(&action.name).cloned()),
+                        action.name.clone(),
+                    )
+                });
+            })
+            .ok();
+
+            let candidates = actions
+                .iter()
+                .enumerate()
+                .map(|(ix, command)| StringMatchCandidate {
+                    id: ix,
+                    string: command.name.to_string(),
+                    char_bag: command.name.chars().collect(),
+                })
+                .collect::<Vec<_>>();
+            let mut 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_executor().clone(),
+                )
+                .await
+            };
+            let mut intercept_result = None;
+            // todo!() for vim mode
+            // cx.read(|cx| {
+            //     if cx.has_global::<CommandPaletteInterceptor>() {
+            //         cx.global::<CommandPaletteInterceptor>()(&query, cx)
+            //     } else {
+            //         None
+            //     }
+            // });
+            if *RELEASE_CHANNEL == ReleaseChannel::Dev {
+                if parse_zed_link(&query).is_some() {
+                    intercept_result = Some(CommandInterceptResult {
+                        action: OpenZedURL { url: query.clone() }.boxed_clone(),
+                        string: query.clone(),
+                        positions: vec![],
+                    })
+                }
+            }
+            if let Some(CommandInterceptResult {
+                action,
+                string,
+                positions,
+            }) = intercept_result
+            {
+                if let Some(idx) = matches
+                    .iter()
+                    .position(|m| actions[m.candidate_id].action.type_id() == action.type_id())
+                {
+                    matches.remove(idx);
+                }
+                actions.push(Command {
+                    name: string.clone(),
+                    action,
+                    keystrokes: vec![],
+                });
+                matches.insert(
+                    0,
+                    StringMatch {
+                        candidate_id: actions.len() - 1,
+                        string,
+                        positions,
+                        score: 0.0,
+                    },
+                )
+            }
+            picker
+                .update(&mut cx, |picker, _| {
+                    let delegate = &mut picker.delegate;
+                    delegate.actions = actions;
+                    delegate.matches = matches;
+                    if delegate.matches.is_empty() {
+                        delegate.selected_ix = 0;
+                    } else {
+                        delegate.selected_ix =
+                            cmp::min(delegate.selected_ix, delegate.matches.len() - 1);
+                    }
+                })
+                .log_err();
+        })
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+        dbg!("dismissed");
+        self.command_palette
+            .update(cx, |command_palette, cx| cx.emit(ModalEvent::Dismissed))
+            .log_err();
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
+        // if !self.matches.is_empty() {
+        //     let window = cx.window();
+        //     let focused_view_id = self.focused_view_id;
+        //     let action_ix = self.matches[self.selected_ix].candidate_id;
+        //     let command = self.actions.remove(action_ix);
+        //     cx.update_default_global(|hit_counts: &mut HitCounts, _| {
+        //         *hit_counts.0.entry(command.name).or_default() += 1;
+        //     });
+        //     let action = command.action;
+
+        //     cx.app_context()
+        //         .spawn(move |mut cx| async move {
+        //             window
+        //                 .dispatch_action(focused_view_id, action.as_ref(), &mut cx)
+        //                 .ok_or_else(|| anyhow!("window was closed"))
+        //         })
+        //         .detach_and_log_err(cx);
+        // }
+        self.dismissed(cx)
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Self::ListItem {
+        div().child("ooh yeah")
+    }
+
+    // fn render_match(
+    //     &self,
+    //     ix: usize,
+    //     mouse_state: &mut MouseState,
+    //     selected: bool,
+    //     cx: &gpui::AppContext,
+    // ) -> AnyElement<Picker<Self>> {
+    //     let mat = &self.matches[ix];
+    //     let command = &self.actions[mat.candidate_id];
+    //     let theme = theme::current(cx);
+    //     let style = theme.picker.item.in_state(selected).style_for(mouse_state);
+    //     let key_style = &theme.command_palette.key.in_state(selected);
+    //     let keystroke_spacing = theme.command_palette.keystroke_spacing;
+
+    //     Flex::row()
+    //         .with_child(
+    //             Label::new(mat.string.clone(), style.label.clone())
+    //                 .with_highlights(mat.positions.clone()),
+    //         )
+    //         .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, key_style.label.clone())
+    //                                     .contained()
+    //                                     .with_style(key_style.container),
+    //                             )
+    //                         } else {
+    //                             None
+    //                         }
+    //                     }),
+    //                 )
+    //                 .with_child(
+    //                     Label::new(keystroke.key.clone(), key_style.label.clone())
+    //                         .contained()
+    //                         .with_style(key_style.container),
+    //                 )
+    //                 .contained()
+    //                 .with_margin_left(keystroke_spacing)
+    //                 .flex_float()
+    //         }))
+    //         .contained()
+    //         .with_style(style.container)
+    //         .into_any()
+    // }
+}
+
+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);
+    for char in name.chars() {
+        if char == ':' {
+            if result.ends_with(':') {
+                result.push(' ');
+            } else {
+                result.push(':');
+            }
+        } else if char == '_' {
+            result.push(' ');
+        } else if char.is_uppercase() {
+            if !result.ends_with(' ') {
+                result.push(' ');
+            }
+            result.extend(char.to_lowercase());
+        } else {
+            result.push(char);
+        }
+    }
+    result
+}
+
+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()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::sync::Arc;
+
+    use super::*;
+    use editor::Editor;
+    use gpui::{executor::Deterministic, TestAppContext};
+    use project::Project;
+    use workspace::{AppState, Workspace};
+
+    #[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"
+        );
+        assert_eq!(
+            humanize_action_name("go_to_line::Deploy"),
+            "go to line: deploy"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
+        let editor = window.add_view(cx, |cx| {
+            let mut editor = Editor::single_line(None, cx);
+            editor.set_text("abc", cx);
+            editor
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            cx.focus(&editor);
+            workspace.add_item(Box::new(editor.clone()), cx)
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            toggle_command_palette(workspace, &Toggle, cx);
+        });
+
+        let palette = workspace.read_with(cx, |workspace, _| {
+            workspace.modal::<CommandPalette>().unwrap()
+        });
+
+        palette
+            .update(cx, |palette, cx| {
+                // Fill up palette's command list by running an empty query;
+                // we only need it to subsequently assert that the palette is initially
+                // sorted by command's name.
+                palette.delegate_mut().update_matches("".to_string(), cx)
+            })
+            .await;
+
+        palette.update(cx, |palette, _| {
+            let is_sorted =
+                |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
+            assert!(is_sorted(&palette.delegate().actions));
+        });
+
+        palette
+            .update(cx, |palette, cx| {
+                palette
+                    .delegate_mut()
+                    .update_matches("bcksp".to_string(), cx)
+            })
+            .await;
+
+        palette.update(cx, |palette, cx| {
+            assert_eq!(palette.delegate().matches[0].string, "editor: backspace");
+            palette.confirm(&Default::default(), cx);
+        });
+        deterministic.run_until_parked();
+        editor.read_with(cx, |editor, cx| {
+            assert_eq!(editor.text(cx), "ab");
+        });
+
+        // Add namespace filter, and redeploy the palette
+        cx.update(|cx| {
+            cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
+                filter.filtered_namespaces.insert("editor");
+            })
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            toggle_command_palette(workspace, &Toggle, cx);
+        });
+
+        // Assert editor command not present
+        let palette = workspace.read_with(cx, |workspace, _| {
+            workspace.modal::<CommandPalette>().unwrap()
+        });
+
+        palette
+            .update(cx, |palette, cx| {
+                palette
+                    .delegate_mut()
+                    .update_matches("bcksp".to_string(), cx)
+            })
+            .await;
+
+        palette.update(cx, |palette, _| {
+            assert!(palette.delegate().matches.is_empty())
+        });
+    }
+
+    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+        cx.update(|cx| {
+            let app_state = AppState::test(cx);
+            theme::init(cx);
+            language::init(cx);
+            editor::init(cx);
+            workspace::init(app_state.clone(), cx);
+            init(cx);
+            Project::init_settings(cx);
+            app_state
+        })
+    }
+}

crates/gpui2/src/action.rs 🔗

@@ -4,7 +4,7 @@ use collections::{HashMap, HashSet};
 use lazy_static::lazy_static;
 use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
 use serde::Deserialize;
-use std::any::{type_name, Any};
+use std::any::{type_name, Any, TypeId};
 
 /// Actions are used to implement keyboard-driven UI.
 /// When you declare an action, you can bind keys to the action in the keymap and
@@ -100,6 +100,11 @@ where
     }
 }
 
+impl dyn Action {
+    pub fn type_id(&self) -> TypeId {
+        self.as_any().type_id()
+    }
+}
 type ActionBuilder = fn(json: Option<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
 
 lazy_static! {

crates/workspace2/src/workspace2.rs 🔗

@@ -37,10 +37,10 @@ use futures::{
 };
 use gpui::{
     actions, div, point, rems, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
-    AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, FocusHandle,
-    GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, StatefulInteractive,
-    Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds,
-    WindowContext, WindowHandle, WindowOptions,
+    AsyncWindowContext, Bounds, Component, DispatchContext, Div, Entity, EntityId, EventEmitter,
+    FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size,
+    StatefulInteractive, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+    WindowBounds, WindowContext, WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -3709,157 +3709,160 @@ impl Render for Workspace {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        div()
-            .relative()
-            .size_full()
-            .flex()
-            .flex_col()
-            .font("Zed Sans")
-            .gap_0()
-            .justify_start()
-            .items_start()
-            .text_color(cx.theme().colors().text)
-            .bg(cx.theme().colors().background)
-            .child(self.render_titlebar(cx))
-            .child(
-                // todo! should this be a component a view?
-                self.modal_layer
-                    .wrapper_element(cx)
-                    .relative()
-                    .flex_1()
-                    .w_full()
-                    .flex()
-                    .overflow_hidden()
-                    .border_t()
-                    .border_b()
-                    .border_color(cx.theme().colors().border)
-                    // .children(
-                    //     Some(
-                    //         Panel::new("project-panel-outer", cx)
-                    //             .side(PanelSide::Left)
-                    //             .child(ProjectPanel::new("project-panel-inner")),
-                    //     )
-                    //     .filter(|_| self.is_project_panel_open()),
-                    // )
-                    // .children(
-                    //     Some(
-                    //         Panel::new("collab-panel-outer", cx)
-                    //             .child(CollabPanel::new("collab-panel-inner"))
-                    //             .side(PanelSide::Left),
-                    //     )
-                    //     .filter(|_| self.is_collab_panel_open()),
-                    // )
-                    // .child(NotificationToast::new(
-                    //     "maxbrunsfeld has requested to add you as a contact.".into(),
-                    // ))
-                    .child(
-                        div().flex().flex_col().flex_1().h_full().child(
-                            div().flex().flex_1().child(self.center.render(
-                                &self.project,
-                                &self.follower_states,
-                                self.active_call(),
-                                &self.active_pane,
-                                self.zoomed.as_ref(),
-                                &self.app_state,
-                                cx,
-                            )),
+        let mut context = DispatchContext::default();
+        context.insert("Workspace");
+        cx.with_key_dispatch_context(context, |cx| {
+            div()
+                .relative()
+                .size_full()
+                .flex()
+                .flex_col()
+                .font("Zed Sans")
+                .gap_0()
+                .justify_start()
+                .items_start()
+                .text_color(cx.theme().colors().text)
+                .bg(cx.theme().colors().background)
+                .child(self.render_titlebar(cx))
+                .child(
+                    // todo! should this be a component a view?
+                    self.modal_layer
+                        .wrapper_element(cx)
+                        .relative()
+                        .flex_1()
+                        .w_full()
+                        .flex()
+                        .overflow_hidden()
+                        .border_t()
+                        .border_b()
+                        .border_color(cx.theme().colors().border)
+                        // .children(
+                        //     Some(
+                        //         Panel::new("project-panel-outer", cx)
+                        //             .side(PanelSide::Left)
+                        //             .child(ProjectPanel::new("project-panel-inner")),
+                        //     )
+                        //     .filter(|_| self.is_project_panel_open()),
+                        // )
+                        // .children(
+                        //     Some(
+                        //         Panel::new("collab-panel-outer", cx)
+                        //             .child(CollabPanel::new("collab-panel-inner"))
+                        //             .side(PanelSide::Left),
+                        //     )
+                        //     .filter(|_| self.is_collab_panel_open()),
+                        // )
+                        // .child(NotificationToast::new(
+                        //     "maxbrunsfeld has requested to add you as a contact.".into(),
+                        // ))
+                        .child(
+                            div().flex().flex_col().flex_1().h_full().child(
+                                div().flex().flex_1().child(self.center.render(
+                                    &self.project,
+                                    &self.follower_states,
+                                    self.active_call(),
+                                    &self.active_pane,
+                                    self.zoomed.as_ref(),
+                                    &self.app_state,
+                                    cx,
+                                )),
+                            ), // .children(
+                               //     Some(
+                               //         Panel::new("terminal-panel", cx)
+                               //             .child(Terminal::new())
+                               //             .allowed_sides(PanelAllowedSides::BottomOnly)
+                               //             .side(PanelSide::Bottom),
+                               //     )
+                               //     .filter(|_| self.is_terminal_open()),
+                               // ),
                         ), // .children(
                            //     Some(
-                           //         Panel::new("terminal-panel", cx)
-                           //             .child(Terminal::new())
-                           //             .allowed_sides(PanelAllowedSides::BottomOnly)
-                           //             .side(PanelSide::Bottom),
+                           //         Panel::new("chat-panel-outer", cx)
+                           //             .side(PanelSide::Right)
+                           //             .child(ChatPanel::new("chat-panel-inner").messages(vec![
+                           //                 ChatMessage::new(
+                           //                     "osiewicz".to_string(),
+                           //                     "is this thing on?".to_string(),
+                           //                     DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
+                           //                         .unwrap()
+                           //                         .naive_local(),
+                           //                 ),
+                           //                 ChatMessage::new(
+                           //                     "maxdeviant".to_string(),
+                           //                     "Reading you loud and clear!".to_string(),
+                           //                     DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
+                           //                         .unwrap()
+                           //                         .naive_local(),
+                           //                 ),
+                           //             ])),
                            //     )
-                           //     .filter(|_| self.is_terminal_open()),
+                           //     .filter(|_| self.is_chat_panel_open()),
+                           // )
+                           // .children(
+                           //     Some(
+                           //         Panel::new("notifications-panel-outer", cx)
+                           //             .side(PanelSide::Right)
+                           //             .child(NotificationsPanel::new("notifications-panel-inner")),
+                           //     )
+                           //     .filter(|_| self.is_notifications_panel_open()),
+                           // )
+                           // .children(
+                           //     Some(
+                           //         Panel::new("assistant-panel-outer", cx)
+                           //             .child(AssistantPanel::new("assistant-panel-inner")),
+                           //     )
+                           //     .filter(|_| self.is_assistant_panel_open()),
                            // ),
-                    ), // .children(
-                       //     Some(
-                       //         Panel::new("chat-panel-outer", cx)
-                       //             .side(PanelSide::Right)
-                       //             .child(ChatPanel::new("chat-panel-inner").messages(vec![
-                       //                 ChatMessage::new(
-                       //                     "osiewicz".to_string(),
-                       //                     "is this thing on?".to_string(),
-                       //                     DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
-                       //                         .unwrap()
-                       //                         .naive_local(),
-                       //                 ),
-                       //                 ChatMessage::new(
-                       //                     "maxdeviant".to_string(),
-                       //                     "Reading you loud and clear!".to_string(),
-                       //                     DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
-                       //                         .unwrap()
-                       //                         .naive_local(),
-                       //                 ),
-                       //             ])),
-                       //     )
-                       //     .filter(|_| self.is_chat_panel_open()),
-                       // )
-                       // .children(
-                       //     Some(
-                       //         Panel::new("notifications-panel-outer", cx)
-                       //             .side(PanelSide::Right)
-                       //             .child(NotificationsPanel::new("notifications-panel-inner")),
-                       //     )
-                       //     .filter(|_| self.is_notifications_panel_open()),
-                       // )
-                       // .children(
-                       //     Some(
-                       //         Panel::new("assistant-panel-outer", cx)
-                       //             .child(AssistantPanel::new("assistant-panel-inner")),
-                       //     )
-                       //     .filter(|_| self.is_assistant_panel_open()),
-                       // ),
-            )
-            .child(self.status_bar.clone())
-            // .when(self.debug.show_toast, |this| {
-            //     this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast")))
-            // })
-            // .children(
-            //     Some(
-            //         div()
-            //             .absolute()
-            //             .top(px(50.))
-            //             .left(px(640.))
-            //             .z_index(8)
-            //             .child(LanguageSelector::new("language-selector")),
-            //     )
-            //     .filter(|_| self.is_language_selector_open()),
-            // )
-            .z_index(8)
-            // Debug
-            .child(
-                div()
-                    .flex()
-                    .flex_col()
-                    .z_index(9)
-                    .absolute()
-                    .top_20()
-                    .left_1_4()
-                    .w_40()
-                    .gap_2(), // .when(self.show_debug, |this| {
-                              //     this.child(Button::<Workspace>::new("Toggle User Settings").on_click(
-                              //         Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)),
-                              //     ))
-                              //     .child(
-                              //         Button::<Workspace>::new("Toggle Toasts").on_click(Arc::new(
-                              //             |workspace, cx| workspace.debug_toggle_toast(cx),
-                              //         )),
-                              //     )
-                              //     .child(
-                              //         Button::<Workspace>::new("Toggle Livestream").on_click(Arc::new(
-                              //             |workspace, cx| workspace.debug_toggle_livestream(cx),
-                              //         )),
-                              //     )
-                              // })
-                              // .child(
-                              //     Button::<Workspace>::new("Toggle Debug")
-                              //         .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))),
-                              // ),
-            )
+                )
+                .child(self.status_bar.clone())
+                // .when(self.debug.show_toast, |this| {
+                //     this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast")))
+                // })
+                // .children(
+                //     Some(
+                //         div()
+                //             .absolute()
+                //             .top(px(50.))
+                //             .left(px(640.))
+                //             .z_index(8)
+                //             .child(LanguageSelector::new("language-selector")),
+                //     )
+                //     .filter(|_| self.is_language_selector_open()),
+                // )
+                .z_index(8)
+                // Debug
+                .child(
+                    div()
+                        .flex()
+                        .flex_col()
+                        .z_index(9)
+                        .absolute()
+                        .top_20()
+                        .left_1_4()
+                        .w_40()
+                        .gap_2(), // .when(self.show_debug, |this| {
+                                  //     this.child(Button::<Workspace>::new("Toggle User Settings").on_click(
+                                  //         Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)),
+                                  //     ))
+                                  //     .child(
+                                  //         Button::<Workspace>::new("Toggle Toasts").on_click(Arc::new(
+                                  //             |workspace, cx| workspace.debug_toggle_toast(cx),
+                                  //         )),
+                                  //     )
+                                  //     .child(
+                                  //         Button::<Workspace>::new("Toggle Livestream").on_click(Arc::new(
+                                  //             |workspace, cx| workspace.debug_toggle_livestream(cx),
+                                  //         )),
+                                  //     )
+                                  // })
+                                  // .child(
+                                  //     Button::<Workspace>::new("Toggle Debug")
+                                  //         .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))),
+                                  // ),
+                )
+        })
     }
 }
-
 // todo!()
 // impl Entity for Workspace {
 //     type Event = Event;

crates/zed2/Cargo.toml 🔗

@@ -25,7 +25,7 @@ call = { package = "call2", path = "../call2" }
 cli = { path = "../cli" }
 # collab_ui = { path = "../collab_ui" }
 collections = { path = "../collections" }
-# command_palette = { path = "../command_palette" }
+command_palette = { package="command_palette2", path = "../command_palette2" }
 # component_test = { path = "../component_test" }
 # context_menu = { path = "../context_menu" }
 client = { package = "client2", path = "../client2" }
@@ -74,7 +74,7 @@ util = { path = "../util" }
 # vim = { path = "../vim" }
 workspace = { package = "workspace2", path = "../workspace2" }
 # welcome = { path = "../welcome" }
-# zed-actions = {path = "../zed-actions"}
+zed_actions = {package = "zed_actions2", path = "../zed_actions2"}
 anyhow.workspace = true
 async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
 async-tar = "0.4.2"

crates/zed2/src/main.rs 🔗

@@ -142,7 +142,7 @@ fn main() {
         // context_menu::init(cx);
         project::Project::init(&client, cx);
         client::init(&client, cx);
-        // command_palette::init(cx);
+        command_palette::init(cx);
         language::init(cx);
         editor::init(cx);
         copilot::init(
@@ -761,7 +761,7 @@ fn load_embedded_fonts(cx: &AppContext) {
 // #[cfg(not(debug_assertions))]
 // async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
 //     None
-// }
+//
 
 // #[cfg(not(debug_assertions))]
 // fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}

crates/zed_actions2/Cargo.toml 🔗

@@ -0,0 +1,11 @@
+[package]
+name = "zed_actions2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+gpui = { package = "gpui2", path = "../gpui2" }
+serde.workspace = true

crates/zed_actions2/src/lib.rs 🔗

@@ -0,0 +1,34 @@
+use gpui::{action, actions};
+
+actions!(
+    About,
+    DebugElements,
+    DecreaseBufferFontSize,
+    Hide,
+    HideOthers,
+    IncreaseBufferFontSize,
+    Minimize,
+    OpenDefaultKeymap,
+    OpenDefaultSettings,
+    OpenKeymap,
+    OpenLicenses,
+    OpenLocalSettings,
+    OpenLog,
+    OpenSettings,
+    OpenTelemetryLog,
+    Quit,
+    ResetBufferFontSize,
+    ResetDatabase,
+    ShowAll,
+    ToggleFullScreen,
+    Zoom,
+);
+
+#[action]
+pub struct OpenBrowser {
+    pub url: String,
+}
+#[action]
+pub struct OpenZedURL {
+    pub url: String,
+}