From cb160031336be6dcf8e965d66d74e12ed29f5ecd Mon Sep 17 00:00:00 2001 From: Daniel Zhu Date: Fri, 15 Mar 2024 07:22:43 -0700 Subject: [PATCH] Fill context menu of Zed macOS dock icon with recent projects (#8952) Fixes https://github.com/zed-industries/zed/issues/8416 Release Notes: - Added recent projects into Zed's macOS dock icon context menu ([8416](https://github.com/zed-industries/zed/issues/8416)) --------- Co-authored-by: Kirill Bulatov --- Cargo.lock | 2 + crates/gpui/src/app.rs | 11 +++ crates/gpui/src/platform.rs | 2 + crates/gpui/src/platform/mac/platform.rs | 25 +++++- crates/gpui/src/platform/test/platform.rs | 4 + crates/recent_projects/Cargo.toml | 2 + crates/recent_projects/src/recent_projects.rs | 18 ++++- crates/workspace/src/item.rs | 2 +- crates/workspace/src/workspace.rs | 78 ++++++++++++++----- 9 files changed, 121 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc62b9c890..6693f9639a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7600,9 +7600,11 @@ dependencies = [ name = "recent_projects" version = "0.1.0" dependencies = [ + "collections", "editor", "fuzzy", "gpui", + "itertools 0.11.0", "language", "menu", "ordered-float 2.10.0", diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 1c30f16199..dbdd9d7bb8 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1132,6 +1132,17 @@ impl AppContext { self.platform.set_menus(menus, &self.keymap.borrow()); } + /// Adds given path to list of recent paths for the application. + /// The list is usually shown on the application icon's context menu in the dock, + /// and allows to open the recent files via that context menu. + pub fn add_recent_documents(&mut self, paths: &[PathBuf]) { + self.platform.add_recent_documents(paths); + } + + /// Clears the list of recent paths from the application. + pub fn clear_recent_documents(&mut self) { + self.platform.clear_recent_documents(); + } /// Dispatch an action to the currently active window or global action handler /// See [action::Action] for more information on how actions work pub fn dispatch_action(&mut self, action: &dyn Action) { diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index ecebbd2849..77f73a6956 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -118,6 +118,8 @@ pub(crate) trait Platform: 'static { fn on_event(&self, callback: Box bool>); fn set_menus(&self, menus: Vec, keymap: &Keymap); + fn add_recent_documents(&self, _paths: &[PathBuf]) {} + fn clear_recent_documents(&self) {} fn on_app_menu_action(&self, callback: Box); fn on_will_open_app_menu(&self, callback: Box); fn on_validate_app_menu_command(&self, callback: Box bool>); diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 427c95df71..25346f7b5b 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -137,6 +137,7 @@ unsafe fn build_classes() { sel!(application:openURLs:), open_urls as extern "C" fn(&mut Object, Sel, id, id), ); + decl.register() } } @@ -766,6 +767,29 @@ impl Platform for MacPlatform { } } + fn add_recent_documents(&self, paths: &[PathBuf]) { + for path in paths { + let Some(path_str) = path.to_str() else { + log::error!("Not adding to recent documents a non-unicode path: {path:?}"); + continue; + }; + unsafe { + let document_controller: id = + msg_send![class!(NSDocumentController), sharedDocumentController]; + let url: id = NSURL::fileURLWithPath_(nil, ns_string(path_str)); + let _: () = msg_send![document_controller, noteNewRecentDocumentURL:url]; + } + } + } + + fn clear_recent_documents(&self) { + unsafe { + let document_controller: id = + msg_send![class!(NSDocumentController), sharedDocumentController]; + let _: () = msg_send![document_controller, clearRecentDocuments:nil]; + } + } + fn local_timezone(&self) -> UtcOffset { unsafe { let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone]; @@ -1062,7 +1086,6 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { unsafe { let app: id = msg_send![APP_CLASS, sharedApplication]; app.setActivationPolicy_(NSApplicationActivationPolicyRegular); - let platform = get_mac_platform(this); let callback = platform.0.lock().finish_launching.take(); if let Some(callback) = callback { diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index ec870fb44d..5e0c214605 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -241,6 +241,10 @@ impl Platform for TestPlatform { fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} + fn add_recent_documents(&self, _paths: &[PathBuf]) {} + + fn clear_recent_documents(&self) {} + fn on_app_menu_action(&self, _callback: Box) {} fn on_will_open_app_menu(&self, _callback: Box) {} diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 3e8f63d133..02ba6ac129 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -13,8 +13,10 @@ path = "src/recent_projects.rs" doctest = false [dependencies] +collections.workspace = true fuzzy.workspace = true gpui.workspace = true +itertools.workspace = true menu.workspace = true ordered-float.workspace = true picker.workspace = true diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 0be20c0f43..3f93ddc576 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -1,8 +1,10 @@ +use collections::HashMap; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, Subscription, Task, View, ViewContext, WeakView, }; +use itertools::Itertools; use ordered_float::OrderedFloat; use picker::{ highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText}, @@ -56,7 +58,6 @@ impl RecentProjects { .recent_workspaces_on_disk() .await .unwrap_or_default(); - this.update(&mut cx, move |this, cx| { this.picker.update(cx, move |picker, cx| { picker.delegate.workspaces = workspaces; @@ -157,7 +158,7 @@ impl RecentProjectsDelegate { fn new(workspace: WeakView, create_new_window: bool, render_paths: bool) -> Self { Self { workspace, - workspaces: vec![], + workspaces: Vec::new(), selected_match_index: 0, matches: Default::default(), create_new_window, @@ -430,7 +431,20 @@ impl RecentProjectsDelegate { .recent_workspaces_on_disk() .await .unwrap_or_default(); + let mut unique_added_paths = HashMap::default(); + for (id, workspace) in &workspaces { + for path in workspace.paths().iter() { + unique_added_paths.insert(path.clone(), id); + } + } + let updated_paths = unique_added_paths + .into_iter() + .sorted_by_key(|(_, id)| *id) + .map(|(path, _)| path) + .collect::>(); this.update(&mut cx, move |picker, cx| { + cx.clear_recent_documents(); + cx.add_recent_documents(&updated_paths); picker.delegate.workspaces = workspaces; picker.delegate.set_selected_index(ix - 1, cx); picker.delegate.reset_selected_match_index = false; diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 1292892479..91e74a96f5 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -564,7 +564,7 @@ impl ItemHandle for View { } cx.defer(|workspace, cx| { - workspace.serialize_workspace(cx); + workspace.serialize_workspace(cx).detach(); }); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5cafb9846d..7edb2aa8c6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -578,7 +578,16 @@ impl Workspace { project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => { this.update_window_title(cx); - this.serialize_workspace(cx); + let workspace_serialization = this.serialize_workspace(cx); + cx.spawn(|workspace, mut cx| async move { + workspace_serialization.await; + workspace + .update(&mut cx, |workspace, cx| { + workspace.refresh_recent_documents(cx) + })? + .await + }) + .detach_and_log_err(cx) } project::Event::DisconnectedFromHost => { @@ -748,15 +757,15 @@ impl Workspace { ThemeSettings::reload_current_theme(cx); }), cx.observe(&left_dock, |this, _, cx| { - this.serialize_workspace(cx); + this.serialize_workspace(cx).detach(); cx.notify(); }), cx.observe(&bottom_dock, |this, _, cx| { - this.serialize_workspace(cx); + this.serialize_workspace(cx).detach(); cx.notify(); }), cx.observe(&right_dock, |this, _, cx| { - this.serialize_workspace(cx); + this.serialize_workspace(cx).detach(); cx.notify(); }), cx.on_release(|this, window, cx| { @@ -913,10 +922,6 @@ impl Workspace { })? }; - window - .update(&mut cx, |_, cx| cx.activate_window()) - .log_err(); - notify_if_database_failed(window, &mut cx); let opened_items = window .update(&mut cx, |_workspace, cx| { @@ -925,6 +930,14 @@ impl Workspace { .await .unwrap_or_default(); + window + .update(&mut cx, |workspace, cx| { + workspace + .refresh_recent_documents(cx) + .detach_and_log_err(cx); + cx.activate_window() + }) + .log_err(); Ok((window, opened_items)) }) } @@ -1764,7 +1777,7 @@ impl Workspace { } cx.notify(); - self.serialize_workspace(cx); + self.serialize_workspace(cx).detach(); } pub fn close_all_docks(&mut self, cx: &mut ViewContext) { @@ -1778,7 +1791,7 @@ impl Workspace { cx.focus_self(); cx.notify(); - self.serialize_workspace(cx); + self.serialize_workspace(cx).detach(); } /// Transfer focus to the panel of the given type. @@ -1823,7 +1836,7 @@ impl Workspace { self.active_pane.update(cx, |pane, cx| pane.focus(cx)) } - self.serialize_workspace(cx); + self.serialize_workspace(cx).detach(); cx.notify(); return panel; } @@ -2377,7 +2390,7 @@ impl Workspace { } } - self.serialize_workspace(cx); + self.serialize_workspace(cx).detach(); } pub fn split_pane( @@ -3333,12 +3346,12 @@ impl Workspace { cx.background_executor() .timer(Duration::from_millis(100)) .await; - this.update(&mut cx, |this, cx| this.serialize_workspace(cx)) + this.update(&mut cx, |this, cx| this.serialize_workspace(cx).detach()) .log_err(); })); } - fn serialize_workspace(&self, cx: &mut WindowContext) { + fn serialize_workspace(&self, cx: &mut WindowContext) -> Task<()> { fn serialize_pane_handle(pane_handle: &View, cx: &WindowContext) -> SerializedPane { let (items, active) = { let pane = pane_handle.read(cx); @@ -3441,7 +3454,6 @@ impl Workspace { if !location.paths().is_empty() { let center_group = build_serialized_pane_group(&self.center.root, cx); let docks = build_serialized_docks(self, cx); - let serialized_workspace = SerializedWorkspace { id: self.database_id, location, @@ -3451,11 +3463,37 @@ impl Workspace { docks, fullscreen: cx.is_fullscreen(), }; - - cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace)) - .detach(); + return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace)); } } + Task::ready(()) + } + + fn refresh_recent_documents(&self, cx: &mut AppContext) -> Task> { + if !self.project.read(cx).is_local() { + return Task::ready(Ok(())); + } + cx.spawn(|cx| async move { + let recents = WORKSPACE_DB + .recent_workspaces_on_disk() + .await + .unwrap_or_default(); + let mut unique_paths = HashMap::default(); + for (id, workspace) in &recents { + for path in workspace.paths().iter() { + unique_paths.insert(path.clone(), id); + } + } + let current_paths = unique_paths + .into_iter() + .sorted_by_key(|(_, id)| *id) + .map(|(path, _)| path) + .collect::>(); + cx.update(|cx| { + cx.clear_recent_documents(); + cx.add_recent_documents(¤t_paths); + }) + }) } pub(crate) fn load_workspace( @@ -3539,7 +3577,9 @@ impl Workspace { })?; // Serialize ourself to make sure our timestamps and any pane / item changes are replicated - workspace.update(&mut cx, |workspace, cx| workspace.serialize_workspace(cx))?; + workspace.update(&mut cx, |workspace, cx| { + workspace.serialize_workspace(cx).detach() + })?; Ok(opened_items) })