mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-27 12:22:49 +03:00
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 <kirill@zed.dev>
This commit is contained in:
parent
5bf0c8ed2d
commit
cb16003133
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -7600,9 +7600,11 @@ dependencies = [
|
|||||||
name = "recent_projects"
|
name = "recent_projects"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"collections",
|
||||||
"editor",
|
"editor",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"itertools 0.11.0",
|
||||||
"language",
|
"language",
|
||||||
"menu",
|
"menu",
|
||||||
"ordered-float 2.10.0",
|
"ordered-float 2.10.0",
|
||||||
|
@ -1132,6 +1132,17 @@ impl AppContext {
|
|||||||
self.platform.set_menus(menus, &self.keymap.borrow());
|
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
|
/// Dispatch an action to the currently active window or global action handler
|
||||||
/// See [action::Action] for more information on how actions work
|
/// See [action::Action] for more information on how actions work
|
||||||
pub fn dispatch_action(&mut self, action: &dyn Action) {
|
pub fn dispatch_action(&mut self, action: &dyn Action) {
|
||||||
|
@ -118,6 +118,8 @@ pub(crate) trait Platform: 'static {
|
|||||||
fn on_event(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>);
|
fn on_event(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>);
|
||||||
|
|
||||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||||
|
fn add_recent_documents(&self, _paths: &[PathBuf]) {}
|
||||||
|
fn clear_recent_documents(&self) {}
|
||||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||||
|
@ -137,6 +137,7 @@ unsafe fn build_classes() {
|
|||||||
sel!(application:openURLs:),
|
sel!(application:openURLs:),
|
||||||
open_urls as extern "C" fn(&mut Object, Sel, id, id),
|
open_urls as extern "C" fn(&mut Object, Sel, id, id),
|
||||||
);
|
);
|
||||||
|
|
||||||
decl.register()
|
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 {
|
fn local_timezone(&self) -> UtcOffset {
|
||||||
unsafe {
|
unsafe {
|
||||||
let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
|
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 {
|
unsafe {
|
||||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||||
app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
|
app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
|
||||||
|
|
||||||
let platform = get_mac_platform(this);
|
let platform = get_mac_platform(this);
|
||||||
let callback = platform.0.lock().finish_launching.take();
|
let callback = platform.0.lock().finish_launching.take();
|
||||||
if let Some(callback) = callback {
|
if let Some(callback) = callback {
|
||||||
|
@ -241,6 +241,10 @@ impl Platform for TestPlatform {
|
|||||||
|
|
||||||
fn set_menus(&self, _menus: Vec<crate::Menu>, _keymap: &Keymap) {}
|
fn set_menus(&self, _menus: Vec<crate::Menu>, _keymap: &Keymap) {}
|
||||||
|
|
||||||
|
fn add_recent_documents(&self, _paths: &[PathBuf]) {}
|
||||||
|
|
||||||
|
fn clear_recent_documents(&self) {}
|
||||||
|
|
||||||
fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
|
fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
|
||||||
|
|
||||||
fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
|
fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
|
||||||
|
@ -13,8 +13,10 @@ path = "src/recent_projects.rs"
|
|||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
collections.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
ordered-float.workspace = true
|
ordered-float.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
use collections::HashMap;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result,
|
AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result,
|
||||||
Subscription, Task, View, ViewContext, WeakView,
|
Subscription, Task, View, ViewContext, WeakView,
|
||||||
};
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use picker::{
|
use picker::{
|
||||||
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
|
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
|
||||||
@ -56,7 +58,6 @@ impl RecentProjects {
|
|||||||
.recent_workspaces_on_disk()
|
.recent_workspaces_on_disk()
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
this.update(&mut cx, move |this, cx| {
|
this.update(&mut cx, move |this, cx| {
|
||||||
this.picker.update(cx, move |picker, cx| {
|
this.picker.update(cx, move |picker, cx| {
|
||||||
picker.delegate.workspaces = workspaces;
|
picker.delegate.workspaces = workspaces;
|
||||||
@ -157,7 +158,7 @@ impl RecentProjectsDelegate {
|
|||||||
fn new(workspace: WeakView<Workspace>, create_new_window: bool, render_paths: bool) -> Self {
|
fn new(workspace: WeakView<Workspace>, create_new_window: bool, render_paths: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
workspace,
|
workspace,
|
||||||
workspaces: vec![],
|
workspaces: Vec::new(),
|
||||||
selected_match_index: 0,
|
selected_match_index: 0,
|
||||||
matches: Default::default(),
|
matches: Default::default(),
|
||||||
create_new_window,
|
create_new_window,
|
||||||
@ -430,7 +431,20 @@ impl RecentProjectsDelegate {
|
|||||||
.recent_workspaces_on_disk()
|
.recent_workspaces_on_disk()
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.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::<Vec<_>>();
|
||||||
this.update(&mut cx, move |picker, cx| {
|
this.update(&mut cx, move |picker, cx| {
|
||||||
|
cx.clear_recent_documents();
|
||||||
|
cx.add_recent_documents(&updated_paths);
|
||||||
picker.delegate.workspaces = workspaces;
|
picker.delegate.workspaces = workspaces;
|
||||||
picker.delegate.set_selected_index(ix - 1, cx);
|
picker.delegate.set_selected_index(ix - 1, cx);
|
||||||
picker.delegate.reset_selected_match_index = false;
|
picker.delegate.reset_selected_match_index = false;
|
||||||
|
@ -564,7 +564,7 @@ impl<T: Item> ItemHandle for View<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cx.defer(|workspace, cx| {
|
cx.defer(|workspace, cx| {
|
||||||
workspace.serialize_workspace(cx);
|
workspace.serialize_workspace(cx).detach();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -578,7 +578,16 @@ impl Workspace {
|
|||||||
|
|
||||||
project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
|
project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
|
||||||
this.update_window_title(cx);
|
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 => {
|
project::Event::DisconnectedFromHost => {
|
||||||
@ -748,15 +757,15 @@ impl Workspace {
|
|||||||
ThemeSettings::reload_current_theme(cx);
|
ThemeSettings::reload_current_theme(cx);
|
||||||
}),
|
}),
|
||||||
cx.observe(&left_dock, |this, _, cx| {
|
cx.observe(&left_dock, |this, _, cx| {
|
||||||
this.serialize_workspace(cx);
|
this.serialize_workspace(cx).detach();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}),
|
}),
|
||||||
cx.observe(&bottom_dock, |this, _, cx| {
|
cx.observe(&bottom_dock, |this, _, cx| {
|
||||||
this.serialize_workspace(cx);
|
this.serialize_workspace(cx).detach();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}),
|
}),
|
||||||
cx.observe(&right_dock, |this, _, cx| {
|
cx.observe(&right_dock, |this, _, cx| {
|
||||||
this.serialize_workspace(cx);
|
this.serialize_workspace(cx).detach();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}),
|
}),
|
||||||
cx.on_release(|this, window, cx| {
|
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);
|
notify_if_database_failed(window, &mut cx);
|
||||||
let opened_items = window
|
let opened_items = window
|
||||||
.update(&mut cx, |_workspace, cx| {
|
.update(&mut cx, |_workspace, cx| {
|
||||||
@ -925,6 +930,14 @@ impl Workspace {
|
|||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.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))
|
Ok((window, opened_items))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1764,7 +1777,7 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
self.serialize_workspace(cx);
|
self.serialize_workspace(cx).detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
@ -1778,7 +1791,7 @@ impl Workspace {
|
|||||||
|
|
||||||
cx.focus_self();
|
cx.focus_self();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
self.serialize_workspace(cx);
|
self.serialize_workspace(cx).detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transfer focus to the panel of the given type.
|
/// 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.active_pane.update(cx, |pane, cx| pane.focus(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
self.serialize_workspace(cx);
|
self.serialize_workspace(cx).detach();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
@ -2377,7 +2390,7 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.serialize_workspace(cx);
|
self.serialize_workspace(cx).detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn split_pane(
|
pub fn split_pane(
|
||||||
@ -3333,12 +3346,12 @@ impl Workspace {
|
|||||||
cx.background_executor()
|
cx.background_executor()
|
||||||
.timer(Duration::from_millis(100))
|
.timer(Duration::from_millis(100))
|
||||||
.await;
|
.await;
|
||||||
this.update(&mut cx, |this, cx| this.serialize_workspace(cx))
|
this.update(&mut cx, |this, cx| this.serialize_workspace(cx).detach())
|
||||||
.log_err();
|
.log_err();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_workspace(&self, cx: &mut WindowContext) {
|
fn serialize_workspace(&self, cx: &mut WindowContext) -> Task<()> {
|
||||||
fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
|
fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
|
||||||
let (items, active) = {
|
let (items, active) = {
|
||||||
let pane = pane_handle.read(cx);
|
let pane = pane_handle.read(cx);
|
||||||
@ -3441,7 +3454,6 @@ impl Workspace {
|
|||||||
if !location.paths().is_empty() {
|
if !location.paths().is_empty() {
|
||||||
let center_group = build_serialized_pane_group(&self.center.root, cx);
|
let center_group = build_serialized_pane_group(&self.center.root, cx);
|
||||||
let docks = build_serialized_docks(self, cx);
|
let docks = build_serialized_docks(self, cx);
|
||||||
|
|
||||||
let serialized_workspace = SerializedWorkspace {
|
let serialized_workspace = SerializedWorkspace {
|
||||||
id: self.database_id,
|
id: self.database_id,
|
||||||
location,
|
location,
|
||||||
@ -3451,11 +3463,37 @@ impl Workspace {
|
|||||||
docks,
|
docks,
|
||||||
fullscreen: cx.is_fullscreen(),
|
fullscreen: cx.is_fullscreen(),
|
||||||
};
|
};
|
||||||
|
return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
|
||||||
cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace))
|
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Task::ready(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_recent_documents(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
|
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::<Vec<_>>();
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.clear_recent_documents();
|
||||||
|
cx.add_recent_documents(¤t_paths);
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn load_workspace(
|
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
|
// 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)
|
Ok(opened_items)
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user