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:
parent
5734ffbb18
commit
a5fe6d1e61
14 changed files with 482 additions and 66 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -17602,6 +17602,7 @@ dependencies = [
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
"util",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"windows 0.61.1",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
"zed_actions",
|
"zed_actions",
|
||||||
]
|
]
|
||||||
|
|
12
Cargo.toml
12
Cargo.toml
|
@ -399,8 +399,12 @@ async-tungstenite = "0.29.1"
|
||||||
async-watch = "0.3.1"
|
async-watch = "0.3.1"
|
||||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||||
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
|
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
|
||||||
aws-credential-types = { version = "1.2.2", features = ["hardcoded-credentials"] }
|
aws-credential-types = { version = "1.2.2", features = [
|
||||||
aws-sdk-bedrockruntime = { version = "1.80.0", features = ["behavior-version-latest"] }
|
"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-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
|
||||||
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
@ -615,12 +619,10 @@ features = [
|
||||||
[workspace.dependencies.windows]
|
[workspace.dependencies.windows]
|
||||||
version = "0.61"
|
version = "0.61"
|
||||||
features = [
|
features = [
|
||||||
"Foundation_Collections",
|
|
||||||
"Foundation_Numerics",
|
"Foundation_Numerics",
|
||||||
"Storage_Search",
|
"Storage_Search",
|
||||||
"Storage_Streams",
|
"Storage_Streams",
|
||||||
"System_Threading",
|
"System_Threading",
|
||||||
"UI_StartScreen",
|
|
||||||
"UI_ViewManagement",
|
"UI_ViewManagement",
|
||||||
"Wdk_System_SystemServices",
|
"Wdk_System_SystemServices",
|
||||||
"Win32_Globalization",
|
"Win32_Globalization",
|
||||||
|
@ -647,6 +649,7 @@ features = [
|
||||||
"Win32_System_SystemInformation",
|
"Win32_System_SystemInformation",
|
||||||
"Win32_System_SystemServices",
|
"Win32_System_SystemServices",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
|
"Win32_System_Variant",
|
||||||
"Win32_System_WinRT",
|
"Win32_System_WinRT",
|
||||||
"Win32_UI_Controls",
|
"Win32_UI_Controls",
|
||||||
"Win32_UI_HiDpi",
|
"Win32_UI_HiDpi",
|
||||||
|
@ -654,6 +657,7 @@ features = [
|
||||||
"Win32_UI_Input_KeyboardAndMouse",
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
"Win32_UI_Shell",
|
"Win32_UI_Shell",
|
||||||
"Win32_UI_Shell_Common",
|
"Win32_UI_Shell_Common",
|
||||||
|
"Win32_UI_Shell_PropertiesSystem",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ use collections::{FxHashMap, FxHashSet, HashMap, VecDeque};
|
||||||
pub use context::*;
|
pub use context::*;
|
||||||
pub use entity_map::*;
|
pub use entity_map::*;
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
|
use smallvec::SmallVec;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub use test_context::*;
|
pub use test_context::*;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
@ -1430,7 +1431,7 @@ impl App {
|
||||||
|
|
||||||
/// Sets the right click menu for the app icon in the dock
|
/// Sets the right click menu for the app icon in the dock
|
||||||
pub fn set_dock_menu(&self, menus: Vec<MenuItem>) {
|
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.
|
/// 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);
|
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
|
/// Dispatch an action to the currently active window or global action handler
|
||||||
/// See [`crate::Action`] for more information on how actions work
|
/// See [`crate::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) {
|
||||||
|
|
|
@ -203,6 +203,13 @@ pub(crate) trait Platform: 'static {
|
||||||
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
|
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
|
||||||
fn perform_dock_menu_action(&self, _action: usize) {}
|
fn perform_dock_menu_action(&self, _action: usize) {}
|
||||||
fn add_recent_document(&self, _path: &Path) {}
|
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_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>);
|
||||||
|
|
|
@ -440,7 +440,9 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||||
self.with_common(|common| Some(common.menus.clone()))
|
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> {
|
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
|
||||||
Err(anyhow::Error::msg(
|
Err(anyhow::Error::msg(
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
mod clipboard;
|
mod clipboard;
|
||||||
|
mod destination_list;
|
||||||
mod direct_write;
|
mod direct_write;
|
||||||
mod dispatcher;
|
mod dispatcher;
|
||||||
mod display;
|
mod display;
|
||||||
|
@ -10,6 +11,7 @@ mod window;
|
||||||
mod wrapper;
|
mod wrapper;
|
||||||
|
|
||||||
pub(crate) use clipboard::*;
|
pub(crate) use clipboard::*;
|
||||||
|
pub(crate) use destination_list::*;
|
||||||
pub(crate) use direct_write::*;
|
pub(crate) use direct_write::*;
|
||||||
pub(crate) use dispatcher::*;
|
pub(crate) use dispatcher::*;
|
||||||
pub(crate) use display::*;
|
pub(crate) use display::*;
|
||||||
|
|
211
crates/gpui/src/platform/windows/destination_list.rs
Normal file
211
crates/gpui/src/platform/windows/destination_list.rs
Normal file
|
@ -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 parking_lot::RwLock;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use windows::{
|
use windows::{
|
||||||
UI::{
|
UI::ViewManagement::UISettings,
|
||||||
StartScreen::{JumpList, JumpListItem},
|
|
||||||
ViewManagement::UISettings,
|
|
||||||
},
|
|
||||||
Win32::{
|
Win32::{
|
||||||
Foundation::*,
|
Foundation::*,
|
||||||
Graphics::{
|
Graphics::{
|
||||||
|
@ -52,7 +49,7 @@ pub(crate) struct WindowsPlatform {
|
||||||
pub(crate) struct WindowsPlatformState {
|
pub(crate) struct WindowsPlatformState {
|
||||||
callbacks: PlatformCallbacks,
|
callbacks: PlatformCallbacks,
|
||||||
menus: Vec<OwnedMenu>,
|
menus: Vec<OwnedMenu>,
|
||||||
dock_menu_actions: Vec<Box<dyn Action>>,
|
jump_list: JumpList,
|
||||||
// NOTE: standard cursor handles don't need to close.
|
// NOTE: standard cursor handles don't need to close.
|
||||||
pub(crate) current_cursor: Option<HCURSOR>,
|
pub(crate) current_cursor: Option<HCURSOR>,
|
||||||
}
|
}
|
||||||
|
@ -70,12 +67,12 @@ struct PlatformCallbacks {
|
||||||
impl WindowsPlatformState {
|
impl WindowsPlatformState {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let callbacks = PlatformCallbacks::default();
|
let callbacks = PlatformCallbacks::default();
|
||||||
let dock_menu_actions = Vec::new();
|
let jump_list = JumpList::new();
|
||||||
let current_cursor = load_cursor(CursorStyle::Arrow);
|
let current_cursor = load_cursor(CursorStyle::Arrow);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
callbacks,
|
callbacks,
|
||||||
dock_menu_actions,
|
jump_list,
|
||||||
current_cursor,
|
current_cursor,
|
||||||
menus: Vec::new(),
|
menus: Vec::new(),
|
||||||
}
|
}
|
||||||
|
@ -189,9 +186,10 @@ impl WindowsPlatform {
|
||||||
let mut lock = self.state.borrow_mut();
|
let mut lock = self.state.borrow_mut();
|
||||||
if let Some(mut callback) = lock.callbacks.app_menu_action.take() {
|
if let Some(mut callback) = lock.callbacks.app_menu_action.take() {
|
||||||
let Some(action) = lock
|
let Some(action) = lock
|
||||||
.dock_menu_actions
|
.jump_list
|
||||||
|
.dock_menus
|
||||||
.get(action_idx)
|
.get(action_idx)
|
||||||
.map(|action| action.boxed_clone())
|
.map(|dock_menu| dock_menu.action.boxed_clone())
|
||||||
else {
|
else {
|
||||||
lock.callbacks.app_menu_action = Some(callback);
|
lock.callbacks.app_menu_action = Some(callback);
|
||||||
log::error!("Dock menu for index {action_idx} not found");
|
log::error!("Dock menu for index {action_idx} not found");
|
||||||
|
@ -254,33 +252,35 @@ impl WindowsPlatform {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configure_jump_list(&self, menus: Vec<MenuItem>) -> Result<()> {
|
fn set_dock_menus(&self, menus: Vec<MenuItem>) {
|
||||||
let jump_list = JumpList::LoadCurrentAsync()?.get()?;
|
|
||||||
let items = jump_list.Items()?;
|
|
||||||
items.Clear()?;
|
|
||||||
let mut actions = Vec::new();
|
let mut actions = Vec::new();
|
||||||
for item in menus.into_iter() {
|
menus.into_iter().for_each(|menu| {
|
||||||
let item = match item {
|
if let Some(dock_menu) = DockMenuItem::new(menu).log_err() {
|
||||||
MenuItem::Separator => JumpListItem::CreateSeparator()?,
|
actions.push(dock_menu);
|
||||||
MenuItem::Submenu(_) => {
|
}
|
||||||
log::error!("Set `MenuItemSubmenu` for dock menu on Windows is not supported.");
|
});
|
||||||
continue;
|
let mut lock = self.state.borrow_mut();
|
||||||
}
|
lock.jump_list.dock_menus = actions;
|
||||||
MenuItem::Action { name, action, .. } => {
|
update_jump_list(&lock.jump_list).log_err();
|
||||||
let idx = actions.len();
|
}
|
||||||
actions.push(action.boxed_clone());
|
|
||||||
let item_args = format!("--dock-action {}", idx);
|
fn update_jump_list(
|
||||||
JumpListItem::CreateWithArguments(
|
&self,
|
||||||
&HSTRING::from(item_args),
|
menus: Vec<MenuItem>,
|
||||||
&HSTRING::from(name.as_ref()),
|
entries: Vec<SmallVec<[PathBuf; 2]>>,
|
||||||
)?
|
) -> Vec<SmallVec<[PathBuf; 2]>> {
|
||||||
}
|
let mut actions = Vec::new();
|
||||||
};
|
menus.into_iter().for_each(|menu| {
|
||||||
items.Append(&item)?;
|
if let Some(dock_menu) = DockMenuItem::new(menu).log_err() {
|
||||||
}
|
actions.push(dock_menu);
|
||||||
jump_list.SaveAsync()?.get()?;
|
}
|
||||||
self.state.borrow_mut().dock_menu_actions = actions;
|
});
|
||||||
Ok(())
|
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) {
|
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)>) {
|
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
||||||
|
@ -673,6 +673,14 @@ impl Platform for WindowsPlatform {
|
||||||
.log_err();
|
.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 {
|
impl Drop for WindowsPlatform {
|
||||||
|
|
|
@ -24,8 +24,8 @@ use std::{
|
||||||
use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container};
|
use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container};
|
||||||
use util::{ResultExt, paths::PathExt};
|
use util::{ResultExt, paths::PathExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
CloseIntent, ModalView, OpenOptions, SerializedWorkspaceLocation, WORKSPACE_DB, Workspace,
|
CloseIntent, HistoryManager, ModalView, OpenOptions, SerializedWorkspaceLocation, WORKSPACE_DB,
|
||||||
WorkspaceId,
|
Workspace, WorkspaceId,
|
||||||
};
|
};
|
||||||
use zed_actions::{OpenRecent, OpenRemote};
|
use zed_actions::{OpenRecent, OpenRemote};
|
||||||
|
|
||||||
|
@ -553,7 +553,13 @@ impl RecentProjectsDelegate {
|
||||||
.delegate
|
.delegate
|
||||||
.set_selected_index(ix.saturating_sub(1), window, cx);
|
.set_selected_index(ix.saturating_sub(1), window, cx);
|
||||||
picker.delegate.reset_selected_match_index = false;
|
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();
|
.detach();
|
||||||
|
|
|
@ -67,6 +67,9 @@ uuid.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
windows.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
call = { workspace = true, features = ["test-support"] }
|
call = { workspace = true, features = ["test-support"] }
|
||||||
client = { 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"] }
|
project = { workspace = true, features = ["test-support"] }
|
||||||
session = { workspace = true, features = ["test-support"] }
|
session = { workspace = true, features = ["test-support"] }
|
||||||
settings = { 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
|
tempfile.workspace = true
|
||||||
|
|
129
crates/workspace/src/history_manager.rs
Normal file
129
crates/workspace/src/history_manager.rs
Normal file
|
@ -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<HistoryManagerEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HistoryManagerEntry {
|
||||||
|
pub id: WorkspaceId,
|
||||||
|
pub path: SmallVec<[PathBuf; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalHistoryManager(Entity<HistoryManager>);
|
||||||
|
|
||||||
|
impl Global for GlobalHistoryManager {}
|
||||||
|
|
||||||
|
impl HistoryManager {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
history: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(this: Entity<HistoryManager>, 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::<Vec<_>>();
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.history = recent_folders;
|
||||||
|
this.update_jump_list(cx);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn global(cx: &App) -> Option<Entity<Self>> {
|
||||||
|
cx.try_global::<GlobalHistoryManager>()
|
||||||
|
.map(|model| model.0.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_global(history_manager: Entity<Self>, 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::<Vec<_>>();
|
||||||
|
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<SmallVec<[PathBuf; 2]>>,
|
||||||
|
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::<SmallVec<[PathBuf; 2]>>();
|
||||||
|
Self { id, path }
|
||||||
|
}
|
||||||
|
}
|
|
@ -745,7 +745,7 @@ impl WorkspaceDb {
|
||||||
conn.exec_bound(sql!(
|
conn.exec_bound(sql!(
|
||||||
DELETE FROM pane_groups WHERE workspace_id = ?1;
|
DELETE FROM pane_groups WHERE workspace_id = ?1;
|
||||||
DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
|
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")?;
|
conn.exec_bound(sql!(DELETE FROM breakpoints WHERE workspace_id = ?1))?(workspace.id).context("Clearing old breakpoints")?;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod dock;
|
pub mod dock;
|
||||||
|
pub mod history_manager;
|
||||||
pub mod item;
|
pub mod item;
|
||||||
mod modal_layer;
|
mod modal_layer;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
|
@ -43,6 +44,7 @@ use gpui::{
|
||||||
WindowHandle, WindowId, WindowOptions, action_as, actions, canvas, impl_action_as,
|
WindowHandle, WindowId, WindowOptions, action_as, actions, canvas, impl_action_as,
|
||||||
impl_actions, point, relative, size, transparent_black,
|
impl_actions, point, relative, size, transparent_black,
|
||||||
};
|
};
|
||||||
|
pub use history_manager::*;
|
||||||
pub use item::{
|
pub use item::{
|
||||||
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
|
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
|
||||||
ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
|
ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
|
||||||
|
@ -387,6 +389,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||||
component::init();
|
component::init();
|
||||||
theme_preview::init(cx);
|
theme_preview::init(cx);
|
||||||
toast_layer::init(cx);
|
toast_layer::init(cx);
|
||||||
|
history_manager::init(cx);
|
||||||
|
|
||||||
cx.on_action(Workspace::close_global);
|
cx.on_action(Workspace::close_global);
|
||||||
cx.on_action(reload);
|
cx.on_action(reload);
|
||||||
|
@ -902,6 +905,9 @@ impl Workspace {
|
||||||
project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
|
project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
|
||||||
this.update_window_title(window, cx);
|
this.update_window_title(window, cx);
|
||||||
this.serialize_workspace(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 => {
|
project::Event::DisconnectedFromHost => {
|
||||||
|
@ -1334,7 +1340,10 @@ impl Workspace {
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
window
|
window
|
||||||
.update(cx, |_, window, _| window.activate_window())
|
.update(cx, |workspace, window, cx| {
|
||||||
|
window.activate_window();
|
||||||
|
workspace.update_history(cx);
|
||||||
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
Ok((window, opened_items))
|
Ok((window, opened_items))
|
||||||
})
|
})
|
||||||
|
@ -4707,19 +4716,7 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = if let Some(ssh_project) = &self.serialized_ssh_project {
|
if let Some(location) = self.serialize_workspace_location(cx) {
|
||||||
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 {
|
|
||||||
let breakpoints = self.project.update(cx, |project, cx| {
|
let breakpoints = self.project.update(cx, |project, cx| {
|
||||||
project.breakpoint_store().read(cx).all_breakpoints(cx)
|
project.breakpoint_store().read(cx).all_breakpoints(cx)
|
||||||
});
|
});
|
||||||
|
@ -4739,13 +4736,42 @@ impl Workspace {
|
||||||
breakpoints,
|
breakpoints,
|
||||||
window_id: Some(window.window_handle().window_id().as_u64()),
|
window_id: Some(window.window_handle().window_id().as_u64()),
|
||||||
};
|
};
|
||||||
|
|
||||||
return window.spawn(cx, async move |_| {
|
return window.spawn(cx, async move |_| {
|
||||||
persistence::DB.save_workspace(serialized_workspace).await
|
persistence::DB.save_workspace(serialized_workspace).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Task::ready(())
|
Task::ready(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serialize_workspace_location(&self, cx: &App) -> Option<SerializedWorkspaceLocation> {
|
||||||
|
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(
|
async fn serialize_items(
|
||||||
this: &WeakEntity<Self>,
|
this: &WeakEntity<Self>,
|
||||||
items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
|
items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
|
||||||
|
@ -6614,6 +6640,7 @@ async fn open_ssh_project_inner(
|
||||||
let mut workspace =
|
let mut workspace =
|
||||||
Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
|
Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
|
||||||
workspace.set_serialized_ssh_project(serialized_ssh_project);
|
workspace.set_serialized_ssh_project(serialized_ssh_project);
|
||||||
|
workspace.update_history(cx);
|
||||||
workspace
|
workspace
|
||||||
});
|
});
|
||||||
})?;
|
})?;
|
||||||
|
|
|
@ -26,9 +26,9 @@ use git_ui::git_panel::GitPanel;
|
||||||
use git_ui::project_diff::ProjectDiffToolbar;
|
use git_ui::project_diff::ProjectDiffToolbar;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, App, AppContext as _, AsyncWindowContext, Context, DismissEvent, Element, Entity,
|
Action, App, AppContext as _, AsyncWindowContext, Context, DismissEvent, Element, Entity,
|
||||||
Focusable, KeyBinding, MenuItem, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal,
|
Focusable, KeyBinding, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString,
|
||||||
SharedString, Styled, Task, TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions,
|
Styled, Task, TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, point,
|
||||||
actions, point, px,
|
px,
|
||||||
};
|
};
|
||||||
use image_viewer::ImageInfo;
|
use image_viewer::ImageInfo;
|
||||||
use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
|
use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
|
||||||
|
@ -1386,7 +1386,12 @@ fn reload_keymaps(cx: &mut App, user_key_bindings: Vec<KeyBinding>) {
|
||||||
load_default_keymap(cx);
|
load_default_keymap(cx);
|
||||||
cx.bind_keys(user_key_bindings);
|
cx.bind_keys(user_key_bindings);
|
||||||
cx.set_menus(app_menus());
|
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) {
|
pub fn load_default_keymap(cx: &mut App) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue