diff --git a/Cargo.lock b/Cargo.lock index 3e55bb6f3e..671e33453c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17602,6 +17602,7 @@ dependencies = [ "ui", "util", "uuid", + "windows 0.61.1", "workspace-hack", "zed_actions", ] diff --git a/Cargo.toml b/Cargo.toml index e1299b7451..6802dff21c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -399,8 +399,12 @@ async-tungstenite = "0.29.1" async-watch = "0.3.1" async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] } aws-config = { version = "1.6.1", features = ["behavior-version-latest"] } -aws-credential-types = { version = "1.2.2", features = ["hardcoded-credentials"] } -aws-sdk-bedrockruntime = { version = "1.80.0", features = ["behavior-version-latest"] } +aws-credential-types = { version = "1.2.2", features = [ + "hardcoded-credentials", +] } +aws-sdk-bedrockruntime = { version = "1.80.0", features = [ + "behavior-version-latest", +] } aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] } aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] } base64 = "0.22" @@ -615,12 +619,10 @@ features = [ [workspace.dependencies.windows] version = "0.61" features = [ - "Foundation_Collections", "Foundation_Numerics", "Storage_Search", "Storage_Streams", "System_Threading", - "UI_StartScreen", "UI_ViewManagement", "Wdk_System_SystemServices", "Win32_Globalization", @@ -647,6 +649,7 @@ features = [ "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", + "Win32_System_Variant", "Win32_System_WinRT", "Win32_UI_Controls", "Win32_UI_HiDpi", @@ -654,6 +657,7 @@ features = [ "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_Shell_Common", + "Win32_UI_Shell_PropertiesSystem", "Win32_UI_WindowsAndMessaging", ] diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index c7c2818b7e..525f9d6ac0 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -25,6 +25,7 @@ use collections::{FxHashMap, FxHashSet, HashMap, VecDeque}; pub use context::*; pub use entity_map::*; use http_client::HttpClient; +use smallvec::SmallVec; #[cfg(any(test, feature = "test-support"))] pub use test_context::*; use util::ResultExt; @@ -1430,7 +1431,7 @@ impl App { /// Sets the right click menu for the app icon in the dock pub fn set_dock_menu(&self, menus: Vec) { - self.platform.set_dock_menu(menus, &self.keymap.borrow()); + self.platform.set_dock_menu(menus, &self.keymap.borrow()) } /// Performs the action associated with the given dock menu item, only used on Windows for now. @@ -1446,6 +1447,16 @@ impl App { self.platform.add_recent_document(path); } + /// Updates the jump list with the updated list of recent paths for the application, only used on Windows for now. + /// Note that this also sets the dock menu on Windows. + pub fn update_jump_list( + &self, + menus: Vec, + entries: Vec>, + ) -> Vec> { + self.platform.update_jump_list(menus, entries) + } + /// Dispatch an action to the currently active window or global action handler /// See [`crate::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 4a6ebc92f8..526d992f26 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -203,6 +203,13 @@ pub(crate) trait Platform: 'static { fn set_dock_menu(&self, menu: Vec, keymap: &Keymap); fn perform_dock_menu_action(&self, _action: usize) {} fn add_recent_document(&self, _path: &Path) {} + fn update_jump_list( + &self, + _menus: Vec, + _entries: Vec>, + ) -> Vec> { + Vec::new() + } 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/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index d02eea6dac..445192f07a 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -440,7 +440,9 @@ impl Platform for P { self.with_common(|common| Some(common.menus.clone())) } - fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} + fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) { + // todo(linux) + } fn path_for_auxiliary_executable(&self, _name: &str) -> Result { Err(anyhow::Error::msg( diff --git a/crates/gpui/src/platform/windows.rs b/crates/gpui/src/platform/windows.rs index 51d09f0013..b3a89e9635 100644 --- a/crates/gpui/src/platform/windows.rs +++ b/crates/gpui/src/platform/windows.rs @@ -1,4 +1,5 @@ mod clipboard; +mod destination_list; mod direct_write; mod dispatcher; mod display; @@ -10,6 +11,7 @@ mod window; mod wrapper; pub(crate) use clipboard::*; +pub(crate) use destination_list::*; pub(crate) use direct_write::*; pub(crate) use dispatcher::*; pub(crate) use display::*; diff --git a/crates/gpui/src/platform/windows/destination_list.rs b/crates/gpui/src/platform/windows/destination_list.rs new file mode 100644 index 0000000000..09b47a3ea4 --- /dev/null +++ b/crates/gpui/src/platform/windows/destination_list.rs @@ -0,0 +1,211 @@ +use std::path::PathBuf; + +use itertools::Itertools; +use smallvec::SmallVec; +use windows::{ + Win32::{ + Foundation::PROPERTYKEY, + Globalization::u_strlen, + System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance, StructuredStorage::PROPVARIANT}, + UI::{ + Controls::INFOTIPSIZE, + Shell::{ + Common::{IObjectArray, IObjectCollection}, + DestinationList, EnumerableObjectCollection, ICustomDestinationList, IShellLinkW, + PropertiesSystem::IPropertyStore, + ShellLink, + }, + }, + }, + core::{GUID, HSTRING, Interface}, +}; + +use crate::{Action, MenuItem}; + +pub(crate) struct JumpList { + pub(crate) dock_menus: Vec, + pub(crate) recent_workspaces: Vec>, +} + +impl JumpList { + pub(crate) fn new() -> Self { + Self { + dock_menus: Vec::new(), + recent_workspaces: Vec::new(), + } + } +} + +pub(crate) struct DockMenuItem { + pub(crate) name: String, + pub(crate) description: String, + pub(crate) action: Box, +} + +impl DockMenuItem { + pub(crate) fn new(item: MenuItem) -> anyhow::Result { + match item { + MenuItem::Action { name, action, .. } => Ok(Self { + name: name.clone().into(), + description: if name == "New Window" { + "Opens a new window".to_string() + } else { + name.into() + }, + action, + }), + _ => Err(anyhow::anyhow!( + "Only `MenuItem::Action` is supported for dock menu on Windows." + )), + } + } +} + +// This code is based on the example from Microsoft: +// https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/winui/shell/appshellintegration/RecipePropertyHandler/RecipePropertyHandler.cpp +pub(crate) fn update_jump_list( + jump_list: &JumpList, +) -> anyhow::Result>> { + let (list, removed) = create_destination_list()?; + add_recent_folders(&list, &jump_list.recent_workspaces, removed.as_ref())?; + add_dock_menu(&list, &jump_list.dock_menus)?; + unsafe { list.CommitList() }?; + Ok(removed) +} + +// Copied from: +// https://github.com/microsoft/windows-rs/blob/0fc3c2e5a13d4316d242bdeb0a52af611eba8bd4/crates/libs/windows/src/Windows/Win32/Storage/EnhancedStorage/mod.rs#L1881 +const PKEY_TITLE: PROPERTYKEY = PROPERTYKEY { + fmtid: GUID::from_u128(0xf29f85e0_4ff9_1068_ab91_08002b27b3d9), + pid: 2, +}; + +fn create_destination_list() -> anyhow::Result<(ICustomDestinationList, Vec>)> +{ + let list: ICustomDestinationList = + unsafe { CoCreateInstance(&DestinationList, None, CLSCTX_INPROC_SERVER) }?; + + let mut slots = 0; + let user_removed: IObjectArray = unsafe { list.BeginList(&mut slots) }?; + + let count = unsafe { user_removed.GetCount() }?; + if count == 0 { + return Ok((list, Vec::new())); + } + + let mut removed = Vec::with_capacity(count as usize); + for i in 0..count { + let shell_link: IShellLinkW = unsafe { user_removed.GetAt(i)? }; + let description = { + // INFOTIPSIZE is the maximum size of the buffer + // see https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinkw-getdescription + let mut buffer = [0u16; INFOTIPSIZE as usize]; + unsafe { shell_link.GetDescription(&mut buffer)? }; + let len = unsafe { u_strlen(buffer.as_ptr()) }; + String::from_utf16_lossy(&buffer[..len as usize]) + }; + let args = description.split('\n').map(PathBuf::from).collect(); + + removed.push(args); + } + + Ok((list, removed)) +} + +fn add_dock_menu(list: &ICustomDestinationList, dock_menus: &[DockMenuItem]) -> anyhow::Result<()> { + unsafe { + let tasks: IObjectCollection = + CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?; + for (idx, dock_menu) in dock_menus.iter().enumerate() { + let argument = HSTRING::from(format!("--dock-action {}", idx)); + let description = HSTRING::from(dock_menu.description.as_str()); + let display = dock_menu.name.as_str(); + let task = create_shell_link(argument, description, None, display)?; + tasks.AddObject(&task)?; + } + list.AddUserTasks(&tasks)?; + Ok(()) + } +} + +fn add_recent_folders( + list: &ICustomDestinationList, + entries: &[SmallVec<[PathBuf; 2]>], + removed: &Vec>, +) -> anyhow::Result<()> { + unsafe { + let tasks: IObjectCollection = + CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?; + + for folder_path in entries + .iter() + .filter(|path| !is_item_in_array(path, removed)) + { + let argument = HSTRING::from( + folder_path + .iter() + .map(|path| format!("\"{}\"", path.display())) + .join(" "), + ); + + let description = HSTRING::from( + folder_path + .iter() + .map(|path| path.to_string_lossy()) + .collect::>() + .join("\n"), + ); + // simulate folder icon + // https://github.com/microsoft/vscode/blob/7a5dc239516a8953105da34f84bae152421a8886/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts#L380 + let icon = HSTRING::from("explorer.exe"); + + let display = folder_path + .iter() + .map(|p| { + p.file_name() + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or_else(|| p.to_string_lossy().to_string()) + }) + .join(", "); + + tasks.AddObject(&create_shell_link( + argument, + description, + Some(icon), + &display, + )?)?; + } + + list.AppendCategory(&HSTRING::from("Recent Folders"), &tasks)?; + Ok(()) + } +} + +#[inline] +fn is_item_in_array(item: &SmallVec<[PathBuf; 2]>, removed: &Vec>) -> bool { + removed.iter().any(|removed_item| removed_item == item) +} + +fn create_shell_link( + argument: HSTRING, + description: HSTRING, + icon: Option, + display: &str, +) -> anyhow::Result { + unsafe { + let link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?; + let exe_path = HSTRING::from(std::env::current_exe()?.as_os_str()); + link.SetPath(&exe_path)?; + link.SetArguments(&argument)?; + link.SetDescription(&description)?; + if let Some(icon) = icon { + link.SetIconLocation(&icon, 0)?; + } + let store: IPropertyStore = link.cast()?; + let title = PROPVARIANT::from(display); + store.SetValue(&PKEY_TITLE, &title)?; + store.Commit()?; + + Ok(link) + } +} diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 116b2253d1..7889c89a9e 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -14,10 +14,7 @@ use itertools::Itertools; use parking_lot::RwLock; use smallvec::SmallVec; use windows::{ - UI::{ - StartScreen::{JumpList, JumpListItem}, - ViewManagement::UISettings, - }, + UI::ViewManagement::UISettings, Win32::{ Foundation::*, Graphics::{ @@ -52,7 +49,7 @@ pub(crate) struct WindowsPlatform { pub(crate) struct WindowsPlatformState { callbacks: PlatformCallbacks, menus: Vec, - dock_menu_actions: Vec>, + jump_list: JumpList, // NOTE: standard cursor handles don't need to close. pub(crate) current_cursor: Option, } @@ -70,12 +67,12 @@ struct PlatformCallbacks { impl WindowsPlatformState { fn new() -> Self { let callbacks = PlatformCallbacks::default(); - let dock_menu_actions = Vec::new(); + let jump_list = JumpList::new(); let current_cursor = load_cursor(CursorStyle::Arrow); Self { callbacks, - dock_menu_actions, + jump_list, current_cursor, menus: Vec::new(), } @@ -189,9 +186,10 @@ impl WindowsPlatform { let mut lock = self.state.borrow_mut(); if let Some(mut callback) = lock.callbacks.app_menu_action.take() { let Some(action) = lock - .dock_menu_actions + .jump_list + .dock_menus .get(action_idx) - .map(|action| action.boxed_clone()) + .map(|dock_menu| dock_menu.action.boxed_clone()) else { lock.callbacks.app_menu_action = Some(callback); log::error!("Dock menu for index {action_idx} not found"); @@ -254,33 +252,35 @@ impl WindowsPlatform { false } - fn configure_jump_list(&self, menus: Vec) -> Result<()> { - let jump_list = JumpList::LoadCurrentAsync()?.get()?; - let items = jump_list.Items()?; - items.Clear()?; + fn set_dock_menus(&self, menus: Vec) { let mut actions = Vec::new(); - for item in menus.into_iter() { - let item = match item { - MenuItem::Separator => JumpListItem::CreateSeparator()?, - MenuItem::Submenu(_) => { - log::error!("Set `MenuItemSubmenu` for dock menu on Windows is not supported."); - continue; - } - MenuItem::Action { name, action, .. } => { - let idx = actions.len(); - actions.push(action.boxed_clone()); - let item_args = format!("--dock-action {}", idx); - JumpListItem::CreateWithArguments( - &HSTRING::from(item_args), - &HSTRING::from(name.as_ref()), - )? - } - }; - items.Append(&item)?; - } - jump_list.SaveAsync()?.get()?; - self.state.borrow_mut().dock_menu_actions = actions; - Ok(()) + menus.into_iter().for_each(|menu| { + if let Some(dock_menu) = DockMenuItem::new(menu).log_err() { + actions.push(dock_menu); + } + }); + let mut lock = self.state.borrow_mut(); + lock.jump_list.dock_menus = actions; + update_jump_list(&lock.jump_list).log_err(); + } + + fn update_jump_list( + &self, + menus: Vec, + entries: Vec>, + ) -> Vec> { + let mut actions = Vec::new(); + menus.into_iter().for_each(|menu| { + if let Some(dock_menu) = DockMenuItem::new(menu).log_err() { + actions.push(dock_menu); + } + }); + let mut lock = self.state.borrow_mut(); + lock.jump_list.dock_menus = actions; + lock.jump_list.recent_workspaces = entries; + update_jump_list(&lock.jump_list) + .log_err() + .unwrap_or_default() } } @@ -535,7 +535,7 @@ impl Platform for WindowsPlatform { } fn set_dock_menu(&self, menus: Vec, _keymap: &Keymap) { - self.configure_jump_list(menus).log_err(); + self.set_dock_menus(menus); } fn on_app_menu_action(&self, callback: Box) { @@ -673,6 +673,14 @@ impl Platform for WindowsPlatform { .log_err(); } } + + fn update_jump_list( + &self, + menus: Vec, + entries: Vec>, + ) -> Vec> { + self.update_jump_list(menus, entries) + } } impl Drop for WindowsPlatform { diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 77feba9f2c..3d65bcac02 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -24,8 +24,8 @@ use std::{ use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container}; use util::{ResultExt, paths::PathExt}; use workspace::{ - CloseIntent, ModalView, OpenOptions, SerializedWorkspaceLocation, WORKSPACE_DB, Workspace, - WorkspaceId, + CloseIntent, HistoryManager, ModalView, OpenOptions, SerializedWorkspaceLocation, WORKSPACE_DB, + Workspace, WorkspaceId, }; use zed_actions::{OpenRecent, OpenRemote}; @@ -553,7 +553,13 @@ impl RecentProjectsDelegate { .delegate .set_selected_index(ix.saturating_sub(1), window, cx); picker.delegate.reset_selected_match_index = false; - picker.update_matches(picker.query(cx), window, cx) + picker.update_matches(picker.query(cx), window, cx); + // After deleting a project, we want to update the history manager to reflect the change. + // But we do not emit a update event when user opens a project, because it's handled in `workspace::load_workspace`. + if let Some(history_manager) = HistoryManager::global(cx) { + history_manager + .update(cx, |this, cx| this.delete_history(workspace_id, cx)); + } }) }) .detach(); diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index aa257a5fc9..63a57fe14a 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -67,6 +67,9 @@ uuid.workspace = true zed_actions.workspace = true workspace-hack.workspace = true +[target.'cfg(target_os = "windows")'.dependencies] +windows.workspace = true + [dev-dependencies] call = { workspace = true, features = ["test-support"] } client = { workspace = true, features = ["test-support"] } @@ -78,5 +81,5 @@ gpui = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } session = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } +http_client = { workspace = true, features = ["test-support"] } tempfile.workspace = true diff --git a/crates/workspace/src/history_manager.rs b/crates/workspace/src/history_manager.rs new file mode 100644 index 0000000000..e63b1823ea --- /dev/null +++ b/crates/workspace/src/history_manager.rs @@ -0,0 +1,129 @@ +use std::path::PathBuf; + +use gpui::{AppContext, Entity, Global, MenuItem}; +use smallvec::SmallVec; +use ui::App; +use util::{ResultExt, paths::PathExt}; + +use crate::{NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId}; + +pub fn init(cx: &mut App) { + let manager = cx.new(|_| HistoryManager::new()); + HistoryManager::set_global(manager.clone(), cx); + HistoryManager::init(manager, cx); +} + +pub struct HistoryManager { + /// The history of workspaces that have been opened in the past, in reverse order. + /// The most recent workspace is at the end of the vector. + history: Vec, +} + +#[derive(Debug)] +pub struct HistoryManagerEntry { + pub id: WorkspaceId, + pub path: SmallVec<[PathBuf; 2]>, +} + +struct GlobalHistoryManager(Entity); + +impl Global for GlobalHistoryManager {} + +impl HistoryManager { + fn new() -> Self { + Self { + history: Vec::new(), + } + } + + fn init(this: Entity, cx: &App) { + cx.spawn(async move |cx| { + let recent_folders = WORKSPACE_DB + .recent_workspaces_on_disk() + .await + .unwrap_or_default() + .into_iter() + .rev() + .map(|(id, location)| HistoryManagerEntry::new(id, &location)) + .collect::>(); + this.update(cx, |this, cx| { + this.history = recent_folders; + this.update_jump_list(cx); + }) + }) + .detach(); + } + + pub fn global(cx: &App) -> Option> { + cx.try_global::() + .map(|model| model.0.clone()) + } + + fn set_global(history_manager: Entity, cx: &mut App) { + cx.set_global(GlobalHistoryManager(history_manager)); + } + + pub fn update_history(&mut self, id: WorkspaceId, entry: HistoryManagerEntry, cx: &App) { + if let Some(pos) = self.history.iter().position(|e| e.id == id) { + self.history.remove(pos); + } + self.history.push(entry); + self.update_jump_list(cx); + } + + pub fn delete_history(&mut self, id: WorkspaceId, cx: &App) { + let Some(pos) = self.history.iter().position(|e| e.id == id) else { + return; + }; + self.history.remove(pos); + self.update_jump_list(cx); + } + + fn update_jump_list(&mut self, cx: &App) { + let menus = vec![MenuItem::action("New Window", NewWindow)]; + let entries = self + .history + .iter() + .rev() + .map(|entry| entry.path.clone()) + .collect::>(); + let user_removed = cx.update_jump_list(menus, entries); + self.remove_user_removed_workspaces(user_removed, cx); + } + + pub fn remove_user_removed_workspaces( + &mut self, + user_removed: Vec>, + cx: &App, + ) { + if user_removed.is_empty() { + return; + } + let mut deleted_ids = Vec::new(); + for idx in (0..self.history.len()).rev() { + if let Some(entry) = self.history.get(idx) { + if user_removed.contains(&entry.path) { + deleted_ids.push(entry.id); + self.history.remove(idx); + } + } + } + cx.spawn(async move |_| { + for id in deleted_ids.iter() { + WORKSPACE_DB.delete_workspace_by_id(*id).await.log_err(); + } + }) + .detach(); + } +} + +impl HistoryManagerEntry { + pub fn new(id: WorkspaceId, location: &SerializedWorkspaceLocation) -> Self { + let path = location + .sorted_paths() + .iter() + .map(|path| path.compact()) + .collect::>(); + Self { id, path } + } +} diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 286a744569..06a84773ce 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -745,7 +745,7 @@ impl WorkspaceDb { conn.exec_bound(sql!( DELETE FROM pane_groups WHERE workspace_id = ?1; DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) - .context("Clearing old panes")?; + .context("Clearing old panes")?; conn.exec_bound(sql!(DELETE FROM breakpoints WHERE workspace_id = ?1))?(workspace.id).context("Clearing old breakpoints")?; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 01d836f48d..6c06489c44 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,4 +1,5 @@ pub mod dock; +pub mod history_manager; pub mod item; mod modal_layer; pub mod notifications; @@ -43,6 +44,7 @@ use gpui::{ WindowHandle, WindowId, WindowOptions, action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size, transparent_black, }; +pub use history_manager::*; pub use item::{ FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings, ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle, @@ -387,6 +389,7 @@ pub fn init(app_state: Arc, cx: &mut App) { component::init(); theme_preview::init(cx); toast_layer::init(cx); + history_manager::init(cx); cx.on_action(Workspace::close_global); cx.on_action(reload); @@ -902,6 +905,9 @@ impl Workspace { project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => { this.update_window_title(window, cx); this.serialize_workspace(window, cx); + // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`. + // So we need to update the history. + this.update_history(cx); } project::Event::DisconnectedFromHost => { @@ -1334,7 +1340,10 @@ impl Workspace { .unwrap_or_default(); window - .update(cx, |_, window, _| window.activate_window()) + .update(cx, |workspace, window, cx| { + window.activate_window(); + workspace.update_history(cx); + }) .log_err(); Ok((window, opened_items)) }) @@ -4707,19 +4716,7 @@ impl Workspace { } } - let location = if let Some(ssh_project) = &self.serialized_ssh_project { - Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone())) - } else if let Some(local_paths) = self.local_paths(cx) { - if !local_paths.is_empty() { - Some(SerializedWorkspaceLocation::from_local_paths(local_paths)) - } else { - None - } - } else { - None - }; - - if let Some(location) = location { + if let Some(location) = self.serialize_workspace_location(cx) { let breakpoints = self.project.update(cx, |project, cx| { project.breakpoint_store().read(cx).all_breakpoints(cx) }); @@ -4739,13 +4736,42 @@ impl Workspace { breakpoints, window_id: Some(window.window_handle().window_id().as_u64()), }; + return window.spawn(cx, async move |_| { - persistence::DB.save_workspace(serialized_workspace).await + persistence::DB.save_workspace(serialized_workspace).await; }); } Task::ready(()) } + fn serialize_workspace_location(&self, cx: &App) -> Option { + if let Some(ssh_project) = &self.serialized_ssh_project { + Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone())) + } else if let Some(local_paths) = self.local_paths(cx) { + if !local_paths.is_empty() { + Some(SerializedWorkspaceLocation::from_local_paths(local_paths)) + } else { + None + } + } else { + None + } + } + + fn update_history(&self, cx: &mut App) { + let Some(id) = self.database_id() else { + return; + }; + let Some(location) = self.serialize_workspace_location(cx) else { + return; + }; + if let Some(manager) = HistoryManager::global(cx) { + manager.update(cx, |this, cx| { + this.update_history(id, HistoryManagerEntry::new(id, &location), cx); + }); + } + } + async fn serialize_items( this: &WeakEntity, items_rx: UnboundedReceiver>, @@ -6614,6 +6640,7 @@ async fn open_ssh_project_inner( let mut workspace = Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx); workspace.set_serialized_ssh_project(serialized_ssh_project); + workspace.update_history(cx); workspace }); })?; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index fdf7a88f7b..03a7ad149e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -26,9 +26,9 @@ use git_ui::git_panel::GitPanel; use git_ui::project_diff::ProjectDiffToolbar; use gpui::{ Action, App, AppContext as _, AsyncWindowContext, Context, DismissEvent, Element, Entity, - Focusable, KeyBinding, MenuItem, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, - SharedString, Styled, Task, TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, - actions, point, px, + Focusable, KeyBinding, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString, + Styled, Task, TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, point, + px, }; use image_viewer::ImageInfo; use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType}; @@ -1386,7 +1386,12 @@ fn reload_keymaps(cx: &mut App, user_key_bindings: Vec) { load_default_keymap(cx); cx.bind_keys(user_key_bindings); cx.set_menus(app_menus()); - cx.set_dock_menu(vec![MenuItem::action("New Window", workspace::NewWindow)]); + // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`. + #[cfg(not(target_os = "windows"))] + cx.set_dock_menu(vec![gpui::MenuItem::action( + "New Window", + workspace::NewWindow, + )]); } pub fn load_default_keymap(cx: &mut App) {