Detailed changes
@@ -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",
@@ -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.
@@ -189,6 +189,7 @@ 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 on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
@@ -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;
@@ -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<OwnedMenu>,
+ dock_menu_actions: Vec<Box<dyn Action>>,
// 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<MenuItem>) -> 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<MenuItem>, _keymap: &Keymap) {}
+ fn set_dock_menu(&self, menus: Vec<MenuItem>, _keymap: &Keymap) {
+ self.configure_jump_list(menus).log_err();
+ }
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
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<anyhow::Result<()>> {
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 {
@@ -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<AppState>, 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<usize>,
}
#[derive(Clone, Debug)]
@@ -34,6 +34,7 @@ pub struct OpenRequest {
pub open_channel_notes: Vec<(u64, Option<String>)>,
pub join_channel: Option<u64>,
pub ssh_connection: Option<SshConnectionOptions>,
+ pub dock_menu_action: Option<usize>,
}
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") {
@@ -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<String> {
}
// 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::<IpcHandshake>::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<anyhow::Result<()>> = 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(())
}