Merge pull request #14 from zed-industries/menu-commands

Max Brunsfeld created

Make the application menu dispatch commands on the focused view

Change summary

gpui/src/app.rs                   | 210 +++++++++++++++-----------------
gpui/src/platform/mac/platform.rs |  33 +++--
gpui/src/platform/mac/renderer.rs |  33 ++--
gpui/src/platform/mac/window.rs   |  40 +++--
gpui/src/platform/mod.rs          |  11 
gpui/src/platform/test.rs         |  15 +
zed/src/lib.rs                    |   2 
zed/src/main.rs                   |  26 ---
zed/src/menus.rs                  | 125 ++++++++++--------
zed/src/workspace/mod.rs          |  24 +++
10 files changed, 271 insertions(+), 248 deletions(-)

Detailed changes

gpui/src/app.rs πŸ”—

@@ -69,7 +69,7 @@ pub trait UpdateView {
 
 pub struct Menu<'a> {
     pub name: &'a str,
-    pub items: &'a [MenuItem<'a>],
+    pub items: Vec<MenuItem<'a>>,
 }
 
 pub enum MenuItem<'a> {
@@ -77,6 +77,7 @@ pub enum MenuItem<'a> {
         name: &'a str,
         keystroke: Option<&'a str>,
         action: &'a str,
+        arg: Option<Box<dyn Any + 'static>>,
     },
     Separator,
 }
@@ -127,9 +128,27 @@ impl App {
         let foreground = Rc::new(executor::Foreground::platform(platform.dispatcher())?);
         let app = Self(Rc::new(RefCell::new(MutableAppContext::new(
             foreground,
-            platform,
+            platform.clone(),
             asset_source,
         ))));
+
+        let ctx = app.0.clone();
+        platform.on_menu_command(Box::new(move |command, arg| {
+            let mut ctx = ctx.borrow_mut();
+            if let Some(key_window_id) = ctx.platform.key_window_id() {
+                if let Some((presenter, _)) =
+                    ctx.presenters_and_platform_windows.get(&key_window_id)
+                {
+                    let presenter = presenter.clone();
+                    let path = presenter.borrow().dispatch_path(ctx.as_ref());
+                    if ctx.dispatch_action_any(key_window_id, &path, command, arg.unwrap_or(&())) {
+                        return;
+                    }
+                }
+            }
+            ctx.dispatch_global_action_any(command, arg.unwrap_or(&()));
+        }));
+
         app.0.borrow_mut().weak_self = Some(Rc::downgrade(&app.0));
         Ok(app)
     }
@@ -169,20 +188,6 @@ impl App {
         self
     }
 
-    pub fn on_menu_command<F>(self, mut callback: F) -> Self
-    where
-        F: 'static + FnMut(&str, &mut MutableAppContext),
-    {
-        let ctx = self.0.clone();
-        self.0
-            .borrow()
-            .platform
-            .on_menu_command(Box::new(move |command| {
-                callback(command, &mut *ctx.borrow_mut())
-            }));
-        self
-    }
-
     pub fn on_open_files<F>(self, mut callback: F) -> Self
     where
         F: 'static + FnMut(Vec<PathBuf>, &mut MutableAppContext),
@@ -197,10 +202,6 @@ impl App {
         self
     }
 
-    pub fn set_menus(&self, menus: &[Menu]) {
-        self.0.borrow().platform.set_menus(menus);
-    }
-
     pub fn run<F>(self, on_finish_launching: F)
     where
         F: 'static + FnOnce(&mut MutableAppContext),
@@ -383,8 +384,8 @@ pub struct MutableAppContext {
     subscriptions: HashMap<usize, Vec<Subscription>>,
     observations: HashMap<usize, Vec<Observation>>,
     window_invalidations: HashMap<usize, WindowInvalidation>,
-    invalidation_callbacks:
-        HashMap<usize, Box<dyn FnMut(WindowInvalidation, &mut MutableAppContext)>>,
+    presenters_and_platform_windows:
+        HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
     debug_elements_callbacks: HashMap<usize, Box<dyn Fn(&AppContext) -> crate::json::Value>>,
     foreground: Rc<executor::Foreground>,
     future_handlers: Rc<RefCell<HashMap<usize, FutureHandler>>>,
@@ -422,7 +423,7 @@ impl MutableAppContext {
             subscriptions: HashMap::new(),
             observations: HashMap::new(),
             window_invalidations: HashMap::new(),
-            invalidation_callbacks: HashMap::new(),
+            presenters_and_platform_windows: HashMap::new(),
             debug_elements_callbacks: HashMap::new(),
             foreground,
             future_handlers: Default::default(),
@@ -454,15 +455,6 @@ impl MutableAppContext {
         &self.ctx.background
     }
 
-    pub fn on_window_invalidated<F>(&mut self, window_id: usize, callback: F)
-    where
-        F: 'static + FnMut(WindowInvalidation, &mut MutableAppContext),
-    {
-        self.invalidation_callbacks
-            .insert(window_id, Box::new(callback));
-        self.update_windows();
-    }
-
     pub fn on_debug_elements<F>(&mut self, window_id: usize, callback: F)
     where
         F: 'static + Fn(&AppContext) -> crate::json::Value,
@@ -573,6 +565,10 @@ impl MutableAppContext {
         result
     }
 
+    pub fn set_menus(&self, menus: Vec<Menu>) {
+        self.platform.set_menus(menus);
+    }
+
     pub fn dispatch_action<T: 'static + Any>(
         &mut self,
         window_id: usize,
@@ -634,7 +630,7 @@ impl MutableAppContext {
         }
 
         if !halted_dispatch {
-            self.dispatch_global_action_with_dyn_arg(name, arg);
+            self.dispatch_global_action_any(name, arg);
         }
 
         self.flush_effects();
@@ -642,10 +638,10 @@ impl MutableAppContext {
     }
 
     pub fn dispatch_global_action<T: 'static + Any>(&mut self, name: &str, arg: T) {
-        self.dispatch_global_action_with_dyn_arg(name, Box::new(arg).as_ref());
+        self.dispatch_global_action_any(name, Box::new(arg).as_ref());
     }
 
-    fn dispatch_global_action_with_dyn_arg(&mut self, name: &str, arg: &dyn Any) {
+    fn dispatch_global_action_any(&mut self, name: &str, arg: &dyn Any) {
         if let Some((name, mut handlers)) = self.global_actions.remove_entry(name) {
             self.pending_flushes += 1;
             for handler in handlers.iter_mut().rev() {
@@ -741,87 +737,75 @@ impl MutableAppContext {
     }
 
     fn open_platform_window(&mut self, window_id: usize) {
-        match self.platform.open_window(
+        let mut window = self.platform.open_window(
+            window_id,
             WindowOptions {
                 bounds: RectF::new(vec2f(0., 0.), vec2f(1024., 768.)),
                 title: "Zed".into(),
             },
             self.foreground.clone(),
-        ) {
-            Err(e) => log::error!("error opening window: {}", e),
-            Ok(mut window) => {
-                let text_layout_cache = TextLayoutCache::new(self.platform.fonts());
-                let presenter = Rc::new(RefCell::new(Presenter::new(
-                    window_id,
-                    self.font_cache.clone(),
-                    text_layout_cache,
-                    self.assets.clone(),
-                    self,
-                )));
+        );
+        let text_layout_cache = TextLayoutCache::new(self.platform.fonts());
+        let presenter = Rc::new(RefCell::new(Presenter::new(
+            window_id,
+            self.font_cache.clone(),
+            text_layout_cache,
+            self.assets.clone(),
+            self,
+        )));
 
-                {
-                    let mut app = self.upgrade();
-                    let presenter = presenter.clone();
-                    window.on_event(Box::new(move |event| {
-                        app.update(|ctx| {
-                            if let Event::KeyDown { keystroke, .. } = &event {
-                                if ctx
-                                    .dispatch_keystroke(
-                                        window_id,
-                                        presenter.borrow().dispatch_path(ctx.as_ref()),
-                                        keystroke,
-                                    )
-                                    .unwrap()
-                                {
-                                    return;
-                                }
-                            }
+        {
+            let mut app = self.upgrade();
+            let presenter = presenter.clone();
+            window.on_event(Box::new(move |event| {
+                app.update(|ctx| {
+                    if let Event::KeyDown { keystroke, .. } = &event {
+                        if ctx
+                            .dispatch_keystroke(
+                                window_id,
+                                presenter.borrow().dispatch_path(ctx.as_ref()),
+                                keystroke,
+                            )
+                            .unwrap()
+                        {
+                            return;
+                        }
+                    }
 
-                            let actions =
-                                presenter.borrow_mut().dispatch_event(event, ctx.as_ref());
-                            for action in actions {
-                                ctx.dispatch_action_any(
-                                    window_id,
-                                    &action.path,
-                                    action.name,
-                                    action.arg.as_ref(),
-                                );
-                            }
-                        })
-                    }));
-                }
+                    let actions = presenter.borrow_mut().dispatch_event(event, ctx.as_ref());
+                    for action in actions {
+                        ctx.dispatch_action_any(
+                            window_id,
+                            &action.path,
+                            action.name,
+                            action.arg.as_ref(),
+                        );
+                    }
+                })
+            }));
+        }
 
-                {
-                    let mut app = self.upgrade();
-                    let presenter = presenter.clone();
-                    window.on_resize(Box::new(move |window| {
-                        app.update(|ctx| {
-                            let scene = presenter.borrow_mut().build_scene(
-                                window.size(),
-                                window.scale_factor(),
-                                ctx,
-                            );
-                            window.present_scene(scene);
-                        })
-                    }));
-                }
+        {
+            let mut app = self.upgrade();
+            let presenter = presenter.clone();
+            window.on_resize(Box::new(move |window| {
+                app.update(|ctx| {
+                    let scene = presenter.borrow_mut().build_scene(
+                        window.size(),
+                        window.scale_factor(),
+                        ctx,
+                    );
+                    window.present_scene(scene);
+                })
+            }));
+        }
 
-                {
-                    let presenter = presenter.clone();
-                    self.on_window_invalidated(window_id, move |invalidation, ctx| {
-                        let mut presenter = presenter.borrow_mut();
-                        presenter.invalidate(invalidation, ctx.as_ref());
-                        let scene =
-                            presenter.build_scene(window.size(), window.scale_factor(), ctx);
-                        window.present_scene(scene);
-                    });
-                }
+        self.presenters_and_platform_windows
+            .insert(window_id, (presenter.clone(), window));
 
-                self.on_debug_elements(window_id, move |ctx| {
-                    presenter.borrow().debug_elements(ctx).unwrap()
-                });
-            }
-        }
+        self.on_debug_elements(window_id, move |ctx| {
+            presenter.borrow().debug_elements(ctx).unwrap()
+        });
     }
 
     pub fn add_view<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
@@ -922,9 +906,17 @@ impl MutableAppContext {
         std::mem::swap(&mut invalidations, &mut self.window_invalidations);
 
         for (window_id, invalidation) in invalidations {
-            if let Some(mut callback) = self.invalidation_callbacks.remove(&window_id) {
-                callback(invalidation, self);
-                self.invalidation_callbacks.insert(window_id, callback);
+            if let Some((presenter, mut window)) =
+                self.presenters_and_platform_windows.remove(&window_id)
+            {
+                {
+                    let mut presenter = presenter.borrow_mut();
+                    presenter.invalidate(invalidation, self.as_ref());
+                    let scene = presenter.build_scene(window.size(), window.scale_factor(), self);
+                    window.present_scene(scene);
+                }
+                self.presenters_and_platform_windows
+                    .insert(window_id, (presenter, window));
             }
         }
     }

gpui/src/platform/mac/platform.rs πŸ”—

@@ -1,6 +1,5 @@
 use super::{BoolExt as _, Dispatcher, FontSystem, Window};
 use crate::{executor, keymap::Keystroke, platform, Event, Menu, MenuItem};
-use anyhow::Result;
 use cocoa::{
     appkit::{
         NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
@@ -20,6 +19,7 @@ use objc::{
 };
 use ptr::null_mut;
 use std::{
+    any::Any,
     cell::RefCell,
     ffi::{c_void, CStr},
     os::raw::c_char,
@@ -76,7 +76,7 @@ pub struct MacPlatform {
     dispatcher: Arc<Dispatcher>,
     fonts: Arc<FontSystem>,
     callbacks: RefCell<Callbacks>,
-    menu_item_actions: RefCell<Vec<String>>,
+    menu_item_actions: RefCell<Vec<(String, Option<Box<dyn Any>>)>>,
 }
 
 #[derive(Default)]
@@ -84,7 +84,7 @@ struct Callbacks {
     become_active: Option<Box<dyn FnMut()>>,
     resign_active: Option<Box<dyn FnMut()>>,
     event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
-    menu_command: Option<Box<dyn FnMut(&str)>>,
+    menu_command: Option<Box<dyn FnMut(&str, Option<&dyn Any>)>>,
     open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
     finish_launching: Option<Box<dyn FnOnce() -> ()>>,
 }
@@ -99,7 +99,7 @@ impl MacPlatform {
         }
     }
 
-    unsafe fn create_menu_bar(&self, menus: &[Menu]) -> id {
+    unsafe fn create_menu_bar(&self, menus: Vec<Menu>) -> id {
         let menu_bar = NSMenu::new(nil).autorelease();
         let mut menu_item_actions = self.menu_item_actions.borrow_mut();
         menu_item_actions.clear();
@@ -107,8 +107,9 @@ impl MacPlatform {
         for menu_config in menus {
             let menu_bar_item = NSMenuItem::new(nil).autorelease();
             let menu = NSMenu::new(nil).autorelease();
+            let menu_name = menu_config.name;
 
-            menu.setTitle_(ns_string(menu_config.name));
+            menu.setTitle_(ns_string(menu_name));
 
             for item_config in menu_config.items {
                 let item;
@@ -121,12 +122,13 @@ impl MacPlatform {
                         name,
                         keystroke,
                         action,
+                        arg,
                     } => {
                         if let Some(keystroke) = keystroke {
                             let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
                                 panic!(
                                     "Invalid keystroke for menu item {}:{} - {:?}",
-                                    menu_config.name, name, err
+                                    menu_name, name, err
                                 )
                             });
 
@@ -161,7 +163,7 @@ impl MacPlatform {
 
                         let tag = menu_item_actions.len() as NSInteger;
                         let _: () = msg_send![item, setTag: tag];
-                        menu_item_actions.push(action.to_string());
+                        menu_item_actions.push((action.to_string(), arg));
                     }
                 }
 
@@ -189,7 +191,7 @@ impl platform::Platform for MacPlatform {
         self.callbacks.borrow_mut().event = Some(callback);
     }
 
-    fn on_menu_command(&self, callback: Box<dyn FnMut(&str)>) {
+    fn on_menu_command(&self, callback: Box<dyn FnMut(&str, Option<&dyn Any>)>) {
         self.callbacks.borrow_mut().menu_command = Some(callback);
     }
 
@@ -231,10 +233,15 @@ impl platform::Platform for MacPlatform {
 
     fn open_window(
         &self,
+        id: usize,
         options: platform::WindowOptions,
         executor: Rc<executor::Foreground>,
-    ) -> Result<Box<dyn platform::Window>> {
-        Ok(Box::new(Window::open(options, executor, self.fonts())?))
+    ) -> Box<dyn platform::Window> {
+        Box::new(Window::open(id, options, executor, self.fonts()))
+    }
+
+    fn key_window_id(&self) -> Option<usize> {
+        Window::key_window_id()
     }
 
     fn prompt_for_paths(
@@ -292,7 +299,7 @@ impl platform::Platform for MacPlatform {
         }
     }
 
-    fn set_menus(&self, menus: &[Menu]) {
+    fn set_menus(&self, menus: Vec<Menu>) {
         unsafe {
             let app: id = msg_send![APP_CLASS, sharedApplication];
             app.setMainMenu_(self.create_menu_bar(menus));
@@ -375,8 +382,8 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
         if let Some(callback) = platform.callbacks.borrow_mut().menu_command.as_mut() {
             let tag: NSInteger = msg_send![item, tag];
             let index = tag as usize;
-            if let Some(action) = platform.menu_item_actions.borrow().get(index) {
-                callback(&action);
+            if let Some((action, arg)) = platform.menu_item_actions.borrow().get(index) {
+                callback(action, arg.as_ref().map(Box::as_ref));
             }
         }
     }

gpui/src/platform/mac/renderer.rs πŸ”—

@@ -9,7 +9,6 @@ use crate::{
     scene::Layer,
     Scene,
 };
-use anyhow::{anyhow, Result};
 use cocoa::foundation::NSUInteger;
 use metal::{MTLPixelFormat, MTLResourceOptions, NSRange};
 use shaders::{ToFloat2 as _, ToUchar4 as _};
@@ -41,10 +40,10 @@ impl Renderer {
         device: metal::Device,
         pixel_format: metal::MTLPixelFormat,
         fonts: Arc<dyn platform::FontSystem>,
-    ) -> Result<Self> {
+    ) -> Self {
         let library = device
             .new_library_with_data(SHADERS_METALLIB)
-            .map_err(|message| anyhow!("error building metal library: {}", message))?;
+            .expect("error building metal library");
 
         let unit_vertices = [
             (0., 0.).to_float2(),
@@ -73,7 +72,7 @@ impl Renderer {
             "quad_vertex",
             "quad_fragment",
             pixel_format,
-        )?;
+        );
         let shadow_pipeline_state = build_pipeline_state(
             &device,
             &library,
@@ -81,7 +80,7 @@ impl Renderer {
             "shadow_vertex",
             "shadow_fragment",
             pixel_format,
-        )?;
+        );
         let sprite_pipeline_state = build_pipeline_state(
             &device,
             &library,
@@ -89,7 +88,7 @@ impl Renderer {
             "sprite_vertex",
             "sprite_fragment",
             pixel_format,
-        )?;
+        );
         let path_atlas_pipeline_state = build_path_atlas_pipeline_state(
             &device,
             &library,
@@ -97,8 +96,8 @@ impl Renderer {
             "path_atlas_vertex",
             "path_atlas_fragment",
             MTLPixelFormat::R8Unorm,
-        )?;
-        Ok(Self {
+        );
+        Self {
             sprite_cache,
             path_atlases,
             quad_pipeline_state,
@@ -107,7 +106,7 @@ impl Renderer {
             path_atlas_pipeline_state,
             unit_vertices,
             instances,
-        })
+        }
     }
 
     pub fn render(
@@ -713,13 +712,13 @@ fn build_pipeline_state(
     vertex_fn_name: &str,
     fragment_fn_name: &str,
     pixel_format: metal::MTLPixelFormat,
-) -> Result<metal::RenderPipelineState> {
+) -> metal::RenderPipelineState {
     let vertex_fn = library
         .get_function(vertex_fn_name, None)
-        .map_err(|message| anyhow!("error locating vertex function: {}", message))?;
+        .expect("error locating vertex function");
     let fragment_fn = library
         .get_function(fragment_fn_name, None)
-        .map_err(|message| anyhow!("error locating fragment function: {}", message))?;
+        .expect("error locating fragment function");
 
     let descriptor = metal::RenderPipelineDescriptor::new();
     descriptor.set_label(label);
@@ -737,7 +736,7 @@ fn build_pipeline_state(
 
     device
         .new_render_pipeline_state(&descriptor)
-        .map_err(|message| anyhow!("could not create render pipeline state: {}", message))
+        .expect("could not create render pipeline state")
 }
 
 fn build_path_atlas_pipeline_state(
@@ -747,13 +746,13 @@ fn build_path_atlas_pipeline_state(
     vertex_fn_name: &str,
     fragment_fn_name: &str,
     pixel_format: metal::MTLPixelFormat,
-) -> Result<metal::RenderPipelineState> {
+) -> metal::RenderPipelineState {
     let vertex_fn = library
         .get_function(vertex_fn_name, None)
-        .map_err(|message| anyhow!("error locating vertex function: {}", message))?;
+        .expect("error locating vertex function");
     let fragment_fn = library
         .get_function(fragment_fn_name, None)
-        .map_err(|message| anyhow!("error locating fragment function: {}", message))?;
+        .expect("error locating fragment function");
 
     let descriptor = metal::RenderPipelineDescriptor::new();
     descriptor.set_label(label);
@@ -771,7 +770,7 @@ fn build_path_atlas_pipeline_state(
 
     device
         .new_render_pipeline_state(&descriptor)
-        .map_err(|message| anyhow!("could not create render pipeline state: {}", message))
+        .expect("could not create render pipeline state")
 }
 
 mod shaders {

gpui/src/platform/mac/window.rs πŸ”—

@@ -4,11 +4,10 @@ use crate::{
     platform::{self, Event, WindowContext},
     Scene,
 };
-use anyhow::{anyhow, Result};
 use cocoa::{
     appkit::{
-        NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable, NSViewWidthSizable,
-        NSWindow, NSWindowStyleMask,
+        NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
+        NSViewWidthSizable, NSWindow, NSWindowStyleMask,
     },
     base::{id, nil},
     foundation::{NSAutoreleasePool, NSInteger, NSSize, NSString},
@@ -118,6 +117,7 @@ unsafe fn build_classes() {
 pub struct Window(Rc<RefCell<WindowState>>);
 
 struct WindowState {
+    id: usize,
     native_window: id,
     event_callback: Option<Box<dyn FnMut(Event)>>,
     resize_callback: Option<Box<dyn FnMut(&mut dyn platform::WindowContext)>>,
@@ -131,10 +131,11 @@ struct WindowState {
 
 impl Window {
     pub fn open(
+        id: usize,
         options: platform::WindowOptions,
         executor: Rc<executor::Foreground>,
         fonts: Arc<dyn platform::FontSystem>,
-    ) -> Result<Self> {
+    ) -> Self {
         const PIXEL_FORMAT: metal::MTLPixelFormat = metal::MTLPixelFormat::BGRA8Unorm;
 
         unsafe {
@@ -153,13 +154,10 @@ impl Window {
                 NSBackingStoreBuffered,
                 NO,
             );
+            assert!(!native_window.is_null());
 
-            if native_window == nil {
-                return Err(anyhow!("window returned nil from initializer"));
-            }
-
-            let device = metal::Device::system_default()
-                .ok_or_else(|| anyhow!("could not find default metal device"))?;
+            let device =
+                metal::Device::system_default().expect("could not find default metal device");
 
             let layer: id = msg_send![class!(CAMetalLayer), layer];
             let _: () = msg_send![layer, setDevice: device.as_ptr()];
@@ -175,18 +173,17 @@ impl Window {
 
             let native_view: id = msg_send![VIEW_CLASS, alloc];
             let native_view = NSView::init(native_view);
-            if native_view == nil {
-                return Err(anyhow!("view return nil from initializer"));
-            }
+            assert!(!native_view.is_null());
 
             let window = Self(Rc::new(RefCell::new(WindowState {
+                id,
                 native_window,
                 event_callback: None,
                 resize_callback: None,
                 synthetic_drag_counter: 0,
                 executor,
                 scene_to_render: Default::default(),
-                renderer: Renderer::new(device.clone(), PIXEL_FORMAT, fonts)?,
+                renderer: Renderer::new(device.clone(), PIXEL_FORMAT, fonts),
                 command_queue: device.new_command_queue(),
                 layer,
             })));
@@ -227,7 +224,20 @@ impl Window {
 
             pool.drain();
 
-            Ok(window)
+            window
+        }
+    }
+
+    pub fn key_window_id() -> Option<usize> {
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+            let key_window: id = msg_send![app, keyWindow];
+            if key_window.is_null() {
+                None
+            } else {
+                let id = get_window_state(&*key_window).borrow().id;
+                Some(id)
+            }
         }
     }
 }

gpui/src/platform/mod.rs πŸ”—

@@ -17,13 +17,12 @@ use crate::{
     text_layout::Line,
     Menu, Scene,
 };
-use anyhow::Result;
 use async_task::Runnable;
 pub use event::Event;
-use std::{ops::Range, path::PathBuf, rc::Rc, sync::Arc};
+use std::{any::Any, ops::Range, path::PathBuf, rc::Rc, sync::Arc};
 
 pub trait Platform {
-    fn on_menu_command(&self, callback: Box<dyn FnMut(&str)>);
+    fn on_menu_command(&self, callback: Box<dyn FnMut(&str, Option<&dyn Any>)>);
     fn on_become_active(&self, callback: Box<dyn FnMut()>);
     fn on_resign_active(&self, callback: Box<dyn FnMut()>);
     fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
@@ -36,13 +35,15 @@ pub trait Platform {
     fn activate(&self, ignoring_other_apps: bool);
     fn open_window(
         &self,
+        id: usize,
         options: WindowOptions,
         executor: Rc<executor::Foreground>,
-    ) -> Result<Box<dyn Window>>;
+    ) -> Box<dyn Window>;
+    fn key_window_id(&self) -> Option<usize>;
     fn prompt_for_paths(&self, options: PathPromptOptions) -> Option<Vec<PathBuf>>;
     fn quit(&self);
     fn copy(&self, text: &str);
-    fn set_menus(&self, menus: &[Menu]);
+    fn set_menus(&self, menus: Vec<Menu>);
 }
 
 pub trait Dispatcher: Send + Sync {

gpui/src/platform/test.rs πŸ”—

@@ -1,6 +1,6 @@
 use pathfinder_geometry::vector::Vector2F;
-use std::rc::Rc;
 use std::sync::Arc;
+use std::{any::Any, rc::Rc};
 
 struct Platform {
     dispatcher: Arc<dyn super::Dispatcher>,
@@ -27,7 +27,7 @@ impl Platform {
 }
 
 impl super::Platform for Platform {
-    fn on_menu_command(&self, _: Box<dyn FnMut(&str)>) {}
+    fn on_menu_command(&self, _: Box<dyn FnMut(&str, Option<&dyn Any>)>) {}
 
     fn on_become_active(&self, _: Box<dyn FnMut()>) {}
 
@@ -53,13 +53,18 @@ impl super::Platform for Platform {
 
     fn open_window(
         &self,
+        _: usize,
         options: super::WindowOptions,
         _executor: Rc<super::executor::Foreground>,
-    ) -> anyhow::Result<Box<dyn super::Window>> {
-        Ok(Box::new(Window::new(options.bounds.size())))
+    ) -> Box<dyn super::Window> {
+        Box::new(Window::new(options.bounds.size()))
     }
 
-    fn set_menus(&self, _menus: &[crate::Menu]) {}
+    fn key_window_id(&self) -> Option<usize> {
+        None
+    }
+
+    fn set_menus(&self, _menus: Vec<crate::Menu>) {}
 
     fn quit(&self) {}
 

zed/src/lib.rs πŸ”—

@@ -10,6 +10,6 @@ mod test;
 mod time;
 mod timer;
 mod util;
-mod watch;
+pub mod watch;
 pub mod workspace;
 mod worktree;

zed/src/main.rs πŸ”—

@@ -1,5 +1,4 @@
 use fs::OpenOptions;
-use gpui::PathPromptOptions;
 use log::LevelFilter;
 use simplelog::SimpleLogger;
 use std::{fs, path::PathBuf};
@@ -13,29 +12,8 @@ fn main() {
 
     let app = gpui::App::new(assets::Assets).unwrap();
     let (_, settings_rx) = settings::channel(&app.font_cache()).unwrap();
-    app.set_menus(menus::MENUS);
-    app.on_menu_command({
-        let settings_rx = settings_rx.clone();
-        move |command, ctx| match command {
-            "app:open" => {
-                if let Some(paths) = ctx.platform().prompt_for_paths(PathPromptOptions {
-                    files: true,
-                    directories: true,
-                    multiple: true,
-                }) {
-                    ctx.dispatch_global_action(
-                        "workspace:open_paths",
-                        OpenParams {
-                            paths,
-                            settings: settings_rx.clone(),
-                        },
-                    );
-                }
-            }
-            _ => ctx.dispatch_global_action(command, ()),
-        }
-    })
-    .run(move |ctx| {
+    app.run(move |ctx| {
+        ctx.set_menus(menus::menus(settings_rx.clone()));
         workspace::init(ctx);
         editor::init(ctx);
         file_finder::init(ctx);

zed/src/menus.rs πŸ”—

@@ -1,60 +1,71 @@
+use crate::{settings::Settings, watch::Receiver};
 use gpui::{Menu, MenuItem};
 
 #[cfg(target_os = "macos")]
-pub const MENUS: &'static [Menu] = &[
-    Menu {
-        name: "Zed",
-        items: &[
-            MenuItem::Action {
-                name: "About Zed…",
-                keystroke: None,
-                action: "app:about-zed",
-            },
-            MenuItem::Separator,
-            MenuItem::Action {
-                name: "Quit",
-                keystroke: Some("cmd-q"),
-                action: "app:quit",
-            },
-        ],
-    },
-    Menu {
-        name: "File",
-        items: &[MenuItem::Action {
-            name: "Open…",
-            keystroke: Some("cmd-o"),
-            action: "app:open",
-        }],
-    },
-    Menu {
-        name: "Edit",
-        items: &[
-            MenuItem::Action {
-                name: "Undo",
-                keystroke: Some("cmd-z"),
-                action: "editor:undo",
-            },
-            MenuItem::Action {
-                name: "Redo",
-                keystroke: Some("cmd-Z"),
-                action: "editor:redo",
-            },
-            MenuItem::Separator,
-            MenuItem::Action {
-                name: "Cut",
-                keystroke: Some("cmd-x"),
-                action: "editor:cut",
-            },
-            MenuItem::Action {
-                name: "Copy",
-                keystroke: Some("cmd-c"),
-                action: "editor:copy",
-            },
-            MenuItem::Action {
-                name: "Paste",
-                keystroke: Some("cmd-v"),
-                action: "editor:paste",
-            },
-        ],
-    },
-];
+pub fn menus(settings: Receiver<Settings>) -> Vec<Menu<'static>> {
+    vec![
+        Menu {
+            name: "Zed",
+            items: vec![
+                MenuItem::Action {
+                    name: "About Zed…",
+                    keystroke: None,
+                    action: "app:about-zed",
+                    arg: None,
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Quit",
+                    keystroke: Some("cmd-q"),
+                    action: "app:quit",
+                    arg: None,
+                },
+            ],
+        },
+        Menu {
+            name: "File",
+            items: vec![MenuItem::Action {
+                name: "Open…",
+                keystroke: Some("cmd-o"),
+                action: "workspace:open",
+                arg: Some(Box::new(settings)),
+            }],
+        },
+        Menu {
+            name: "Edit",
+            items: vec![
+                MenuItem::Action {
+                    name: "Undo",
+                    keystroke: Some("cmd-z"),
+                    action: "buffer:undo",
+                    arg: None,
+                },
+                MenuItem::Action {
+                    name: "Redo",
+                    keystroke: Some("cmd-Z"),
+                    action: "buffer:redo",
+                    arg: None,
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Cut",
+                    keystroke: Some("cmd-x"),
+                    action: "buffer:cut",
+                    arg: None,
+                },
+                MenuItem::Action {
+                    name: "Copy",
+                    keystroke: Some("cmd-c"),
+                    action: "buffer:copy",
+                    arg: None,
+                },
+                MenuItem::Action {
+                    name: "Paste",
+                    keystroke: Some("cmd-v"),
+                    action: "buffer:paste",
+                    arg: None,
+                },
+            ],
+        },
+    ]
+}

zed/src/workspace/mod.rs πŸ”—

@@ -8,11 +8,15 @@ pub use pane_group::*;
 pub use workspace::*;
 pub use workspace_view::*;
 
-use crate::{settings::Settings, watch};
-use gpui::MutableAppContext;
+use crate::{
+    settings::Settings,
+    watch::{self, Receiver},
+};
+use gpui::{MutableAppContext, PathPromptOptions};
 use std::path::PathBuf;
 
 pub fn init(app: &mut MutableAppContext) {
+    app.add_global_action("workspace:open", open);
     app.add_global_action("workspace:open_paths", open_paths);
     app.add_global_action("app:quit", quit);
     pane::init(app);
@@ -24,6 +28,22 @@ pub struct OpenParams {
     pub settings: watch::Receiver<Settings>,
 }
 
+fn open(settings: &Receiver<Settings>, ctx: &mut MutableAppContext) {
+    if let Some(paths) = ctx.platform().prompt_for_paths(PathPromptOptions {
+        files: true,
+        directories: true,
+        multiple: true,
+    }) {
+        ctx.dispatch_global_action(
+            "workspace:open_paths",
+            OpenParams {
+                paths,
+                settings: settings.clone(),
+            },
+        );
+    }
+}
+
 fn open_paths(params: &OpenParams, app: &mut MutableAppContext) {
     log::info!("open paths {:?}", params.paths);