History manager (#26369)
While working on implementing `add_recent_documents` for Windows, I found that the process is significantly more complex compared to macOS. On macOS, simply registering the `add_recent_documents` function is enough, as the system handles everything automatically. On Windows, however, there are two cases to consider: - **Files opened by the app**: These appear in the "Recent" section (as shown in the screenshot, "test.txt") and are managed automatically by Windows (by setting windows registry), similar to macOS.  - **Folders opened by the app**: This is more complicated because Windows does not handle it automatically, requiring the application to track opened folders manually. To address this, this PR introduces a `History Manager` along with `HistoryManagerEvent::Update` and `HistoryManagerEvent::Delete` events to simplify the process of managing recently opened folders. https://github.com/user-attachments/assets/a2581c15-7653-4faf-96b0-7c48ab1dcc8d Release Notes: - N/A --------- Co-authored-by: Mikayla Maki <mikayla@zed.dev>
This commit is contained in:
+12
-1
@@ -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<MenuItem>) {
|
||||
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<MenuItem>,
|
||||
entries: Vec<SmallVec<[PathBuf; 2]>>,
|
||||
) -> Vec<SmallVec<[PathBuf; 2]>> {
|
||||
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) {
|
||||
|
||||
@@ -203,6 +203,13 @@ pub(crate) trait Platform: 'static {
|
||||
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
|
||||
fn perform_dock_menu_action(&self, _action: usize) {}
|
||||
fn add_recent_document(&self, _path: &Path) {}
|
||||
fn update_jump_list(
|
||||
&self,
|
||||
_menus: Vec<MenuItem>,
|
||||
_entries: Vec<SmallVec<[PathBuf; 2]>>,
|
||||
) -> Vec<SmallVec<[PathBuf; 2]>> {
|
||||
Vec::new()
|
||||
}
|
||||
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_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||
|
||||
@@ -440,7 +440,9 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
self.with_common(|common| Some(common.menus.clone()))
|
||||
}
|
||||
|
||||
fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
|
||||
fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {
|
||||
// todo(linux)
|
||||
}
|
||||
|
||||
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
|
||||
Err(anyhow::Error::msg(
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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<DockMenuItem>,
|
||||
pub(crate) recent_workspaces: Vec<SmallVec<[PathBuf; 2]>>,
|
||||
}
|
||||
|
||||
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<dyn Action>,
|
||||
}
|
||||
|
||||
impl DockMenuItem {
|
||||
pub(crate) fn new(item: MenuItem) -> anyhow::Result<Self> {
|
||||
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<Vec<SmallVec<[PathBuf; 2]>>> {
|
||||
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<SmallVec<[PathBuf; 2]>>)>
|
||||
{
|
||||
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<SmallVec<[PathBuf; 2]>>,
|
||||
) -> 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::<Vec<_>>()
|
||||
.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<SmallVec<[PathBuf; 2]>>) -> bool {
|
||||
removed.iter().any(|removed_item| removed_item == item)
|
||||
}
|
||||
|
||||
fn create_shell_link(
|
||||
argument: HSTRING,
|
||||
description: HSTRING,
|
||||
icon: Option<HSTRING>,
|
||||
display: &str,
|
||||
) -> anyhow::Result<IShellLinkW> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<OwnedMenu>,
|
||||
dock_menu_actions: Vec<Box<dyn Action>>,
|
||||
jump_list: JumpList,
|
||||
// NOTE: standard cursor handles don't need to close.
|
||||
pub(crate) current_cursor: Option<HCURSOR>,
|
||||
}
|
||||
@@ -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<MenuItem>) -> Result<()> {
|
||||
let jump_list = JumpList::LoadCurrentAsync()?.get()?;
|
||||
let items = jump_list.Items()?;
|
||||
items.Clear()?;
|
||||
fn set_dock_menus(&self, menus: Vec<MenuItem>) {
|
||||
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<MenuItem>,
|
||||
entries: Vec<SmallVec<[PathBuf; 2]>>,
|
||||
) -> Vec<SmallVec<[PathBuf; 2]>> {
|
||||
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<MenuItem>, _keymap: &Keymap) {
|
||||
self.configure_jump_list(menus).log_err();
|
||||
self.set_dock_menus(menus);
|
||||
}
|
||||
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
||||
@@ -673,6 +673,14 @@ impl Platform for WindowsPlatform {
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_jump_list(
|
||||
&self,
|
||||
menus: Vec<MenuItem>,
|
||||
entries: Vec<SmallVec<[PathBuf; 2]>>,
|
||||
) -> Vec<SmallVec<[PathBuf; 2]>> {
|
||||
self.update_jump_list(menus, entries)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WindowsPlatform {
|
||||
|
||||
Reference in New Issue
Block a user