diff --git a/Cargo.toml b/Cargo.toml index 1f6cae0b08..147be29f3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -603,9 +603,11 @@ features = [ version = "0.58" features = [ "implement", + "Foundation_Collections", "Foundation_Numerics", "Storage", "System_Threading", + "UI_StartScreen", "UI_ViewManagement", "Wdk_System_SystemServices", "Win32_Globalization", diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index d5f4e606c7..71df22e95d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1426,6 +1426,11 @@ impl App { 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. + pub fn perform_dock_menu_action(&self, action: usize) { + self.platform.perform_dock_menu_action(action); + } + /// Adds given path to the bottom of the 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. diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index e47907d22e..ae89b5e172 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -189,6 +189,7 @@ 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 on_app_menu_action(&self, callback: Box); fn on_will_open_app_menu(&self, callback: Box); diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index c3350b59a2..62a3d201df 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -22,6 +22,7 @@ use crate::*; pub(crate) const WM_GPUI_CURSOR_STYLE_CHANGED: u32 = WM_USER + 1; pub(crate) const WM_GPUI_CLOSE_ONE_WINDOW: u32 = WM_USER + 2; pub(crate) const WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD: u32 = WM_USER + 3; +pub(crate) const WM_GPUI_DOCK_MENU_ACTION: u32 = WM_USER + 4; const SIZE_MOVE_LOOP_TIMER_ID: usize = 1; const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1; diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 5423dfcbc7..8c8d1a7544 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -25,7 +25,10 @@ use windows::{ System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*}, UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, }, - UI::ViewManagement::UISettings, + UI::{ + StartScreen::{JumpList, JumpListItem}, + ViewManagement::UISettings, + }, }; use crate::{platform::blade::BladeContext, *}; @@ -49,6 +52,7 @@ pub(crate) struct WindowsPlatform { pub(crate) struct WindowsPlatformState { callbacks: PlatformCallbacks, menus: Vec, + dock_menu_actions: Vec>, // NOTE: standard cursor handles don't need to close. pub(crate) current_cursor: HCURSOR, } @@ -66,10 +70,12 @@ struct PlatformCallbacks { impl WindowsPlatformState { fn new() -> Self { let callbacks = PlatformCallbacks::default(); + let dock_menu_actions = Vec::new(); let current_cursor = load_cursor(CursorStyle::Arrow); Self { callbacks, + dock_menu_actions, current_cursor, menus: Vec::new(), } @@ -184,6 +190,24 @@ impl WindowsPlatform { } } + fn handle_dock_action_event(&self, action_idx: usize) { + 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 + .get(action_idx) + .map(|action| action.boxed_clone()) + else { + lock.callbacks.app_menu_action = Some(callback); + log::error!("Dock menu for index {action_idx} not found"); + return; + }; + drop(lock); + callback(&*action); + self.state.borrow_mut().callbacks.app_menu_action = Some(callback); + } + } + // Returns true if the app should quit. fn handle_events(&self) -> bool { let mut msg = MSG::default(); @@ -191,7 +215,9 @@ impl WindowsPlatform { while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { match msg.message { WM_QUIT => return true, - WM_GPUI_CLOSE_ONE_WINDOW | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD => { + WM_GPUI_CLOSE_ONE_WINDOW + | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD + | WM_GPUI_DOCK_MENU_ACTION => { if self.handle_gpui_evnets(msg.message, msg.wParam, msg.lParam, &msg) { return true; } @@ -227,10 +253,40 @@ impl WindowsPlatform { } } WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD => self.run_foreground_task(), + WM_GPUI_DOCK_MENU_ACTION => self.handle_dock_action_event(lparam.0 as _), _ => unreachable!(), } false } + + fn configure_jump_list(&self, menus: Vec) -> Result<()> { + let jump_list = JumpList::LoadCurrentAsync()?.get()?; + let items = jump_list.Items()?; + items.Clear()?; + 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(()) + } } impl Platform for WindowsPlatform { @@ -479,8 +535,9 @@ impl Platform for WindowsPlatform { Some(self.state.borrow().menus.clone()) } - // todo(windows) - fn set_dock_menu(&self, _menus: Vec, _keymap: &Keymap) {} + fn set_dock_menu(&self, menus: Vec, _keymap: &Keymap) { + self.configure_jump_list(menus).log_err(); + } fn on_app_menu_action(&self, callback: Box) { self.state.borrow_mut().callbacks.app_menu_action = Some(callback); @@ -599,6 +656,18 @@ impl Platform for WindowsPlatform { fn register_url_scheme(&self, _: &str) -> Task> { Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) } + + fn perform_dock_menu_action(&self, action: usize) { + unsafe { + PostThreadMessageW( + self.main_thread_id_win32, + WM_GPUI_DOCK_MENU_ACTION, + WPARAM(self.validation_number), + LPARAM(action as isize), + ) + .log_err(); + } + } } impl Drop for WindowsPlatform { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 32fcd6ea09..ed43aabbba 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -217,29 +217,27 @@ fn main() { let (open_listener, mut open_rx) = OpenListener::new(); - let failed_single_instance_check = - if *db::ZED_STATELESS || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev { - false - } else { - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - { - crate::zed::listen_for_cli_connections(open_listener.clone()).is_err() - } + let failed_single_instance_check = if *db::ZED_STATELESS + || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev + { + false + } else { + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + { + crate::zed::listen_for_cli_connections(open_listener.clone()).is_err() + } - #[cfg(target_os = "windows")] - { - !crate::zed::windows_only_instance::check_single_instance( - open_listener.clone(), - args.foreground, - ) - } + #[cfg(target_os = "windows")] + { + !crate::zed::windows_only_instance::check_single_instance(open_listener.clone(), &args) + } - #[cfg(target_os = "macos")] - { - use zed::mac_only_instance::*; - ensure_only_instance() != IsOnlyInstance::Yes - } - }; + #[cfg(target_os = "macos")] + { + use zed::mac_only_instance::*; + ensure_only_instance() != IsOnlyInstance::Yes + } + }; if failed_single_instance_check { println!("zed is already running"); return; @@ -643,6 +641,11 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut return; } + if let Some(action_index) = request.dock_menu_action { + cx.perform_dock_menu_action(action_index); + return; + } + if let Some(connection_options) = request.ssh_connection { cx.spawn(|mut cx| async move { let paths_with_position = @@ -953,7 +956,14 @@ struct Args { /// Run zed in the foreground, only used on Windows, to match the behavior of the behavior on macOS. #[arg(long)] #[cfg(target_os = "windows")] + #[arg(hide = true)] foreground: bool, + + /// The dock action to perform. This is used on Windows only. + #[arg(long)] + #[cfg(target_os = "windows")] + #[arg(hide = true)] + dock_action: Option, } #[derive(Clone, Debug)] diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 4fa22fed79..24a8b2b5ca 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -34,6 +34,7 @@ pub struct OpenRequest { pub open_channel_notes: Vec<(u64, Option)>, pub join_channel: Option, pub ssh_connection: Option, + pub dock_menu_action: Option, } impl OpenRequest { @@ -42,6 +43,8 @@ impl OpenRequest { for url in urls { if let Some(server_name) = url.strip_prefix("zed-cli://") { this.cli_connection = Some(connect_to_cli(server_name)?); + } else if let Some(action_index) = url.strip_prefix("zed-dock-action://") { + this.dock_menu_action = Some(action_index.parse()?); } else if let Some(file) = url.strip_prefix("file://") { this.parse_file_path(file) } else if let Some(file) = url.strip_prefix("zed://file") { diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs index 058507823f..05f24393b0 100644 --- a/crates/zed/src/zed/windows_only_instance.rs +++ b/crates/zed/src/zed/windows_only_instance.rs @@ -1,7 +1,6 @@ use std::{sync::Arc, thread::JoinHandle}; use anyhow::Context; -use clap::Parser; use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake}; use parking_lot::Mutex; use release_channel::app_identifier; @@ -26,23 +25,23 @@ use windows::{ use crate::{Args, OpenListener}; -pub fn check_single_instance(opener: OpenListener, run_foreground: bool) -> bool { +pub fn check_single_instance(opener: OpenListener, args: &Args) -> bool { unsafe { CreateMutexW( None, false, &HSTRING::from(format!("{}-Instance-Mutex", app_identifier())), ) - .expect("Unable to create instance sync event") + .expect("Unable to create instance mutex.") }; let first_instance = unsafe { GetLastError() } != ERROR_ALREADY_EXISTS; if first_instance { // We are the first instance, listen for messages sent from other instances std::thread::spawn(move || with_pipe(|url| opener.open_urls(vec![url]))); - } else if !run_foreground { + } else if !args.foreground { // We are not the first instance, send args to the first instance - send_args_to_instance().log_err(); + send_args_to_instance(args).log_err(); } first_instance @@ -95,31 +94,45 @@ fn retrieve_message_from_pipe_inner(pipe: HANDLE) -> anyhow::Result { } // This part of code is mostly from crates/cli/src/main.rs -fn send_args_to_instance() -> anyhow::Result<()> { - let Args { paths_or_urls, .. } = Args::parse(); +fn send_args_to_instance(args: &Args) -> anyhow::Result<()> { + if let Some(dock_menu_action_idx) = args.dock_action { + let url = format!("zed-dock-action://{}", dock_menu_action_idx); + return write_message_to_instance_pipe(url.as_bytes()); + } + let (server, server_name) = IpcOneShotServer::::new().context("Handshake before Zed spawn")?; let url = format!("zed-cli://{server_name}"); - let mut paths = vec![]; - let mut urls = vec![]; - for path in paths_or_urls.into_iter() { - match std::fs::canonicalize(&path) { - Ok(path) => paths.push(path.to_string_lossy().to_string()), - Err(error) => { - if path.starts_with("zed://") - || path.starts_with("http://") - || path.starts_with("https://") - || path.starts_with("file://") - || path.starts_with("ssh://") - { - urls.push(path); - } else { - log::error!("error parsing path argument: {}", error); + let request = { + let mut paths = vec![]; + let mut urls = vec![]; + for path in args.paths_or_urls.iter() { + match std::fs::canonicalize(&path) { + Ok(path) => paths.push(path.to_string_lossy().to_string()), + Err(error) => { + if path.starts_with("zed://") + || path.starts_with("http://") + || path.starts_with("https://") + || path.starts_with("file://") + || path.starts_with("ssh://") + { + urls.push(path.clone()); + } else { + log::error!("error parsing path argument: {}", error); + } } } } - } + CliRequest::Open { + paths, + urls, + wait: false, + open_new_workspace: None, + env: None, + } + }; + let exit_status = Arc::new(Mutex::new(None)); let sender: JoinHandle> = std::thread::spawn({ let exit_status = exit_status.clone(); @@ -127,13 +140,7 @@ fn send_args_to_instance() -> anyhow::Result<()> { let (_, handshake) = server.accept().context("Handshake after Zed spawn")?; let (tx, rx) = (handshake.requests, handshake.responses); - tx.send(CliRequest::Open { - paths, - urls, - wait: false, - open_new_workspace: None, - env: None, - })?; + tx.send(request)?; while let Ok(response) = rx.recv() { match response { @@ -150,6 +157,15 @@ fn send_args_to_instance() -> anyhow::Result<()> { } }); + write_message_to_instance_pipe(url.as_bytes())?; + sender.join().unwrap()?; + if let Some(exit_status) = exit_status.lock().take() { + std::process::exit(exit_status); + } + Ok(()) +} + +fn write_message_to_instance_pipe(message: &[u8]) -> anyhow::Result<()> { unsafe { let pipe = CreateFileW( &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())), @@ -160,14 +176,8 @@ fn send_args_to_instance() -> anyhow::Result<()> { FILE_FLAGS_AND_ATTRIBUTES::default(), None, )?; - let message = url.as_bytes(); - let mut bytes_written = 0; - WriteFile(pipe, Some(message), Some(&mut bytes_written), None)?; + WriteFile(pipe, Some(message), None, None)?; CloseHandle(pipe)?; } - sender.join().unwrap()?; - if let Some(exit_status) = exit_status.lock().take() { - std::process::exit(exit_status); - } Ok(()) }