Closes #47140 Closes #44691 When launching Zed from the CLI (`zed .`), macOS delivers the path as a `kAEGetURL` Apple Event via Launch Services. Zed relied on the `application:openURLs:` delegate method to receive this, but its timing relative to `applicationDidFinishLaunching:` is not guaranteed by macOS. In release builds, the app reaches `didFinishLaunching` before the URL is delivered, causing it to be missed and a duplicate window to be opened. This does not reproduce in debug builds due to slower initialization, which is why the issue was hard to reproduce from source. This replaces `application:openURLs:` with a custom `kAEGetURL` Apple Event handler registered in `applicationWillFinishLaunching:`. Apple Events are guaranteed to be delivered synchronously between `willFinishLaunching` and `didFinishLaunching`, ensuring the URL is always available before startup logic runs. Release Notes: - Fixed duplicate window creation when opening files/directories from the CLI on macOS.
Unified · Split
| @@ -62,6 +62,9 @@ use util::{ · 61 unmodified lines | |||
| 62 | const NSUTF8StringEncoding: NSUInteger = 4; | 62 | const NSUTF8StringEncoding: NSUInteger = 4; |
| 63 | 63 | ||
| 64 | const MAC_PLATFORM_IVAR: &str = "platform"; | 64 | const MAC_PLATFORM_IVAR: &str = "platform"; |
| 65 | const INTERNET_EVENT_CLASS: u32 = 0x4755524c; // 'GURL' | ||
| 66 | const AE_GET_URL: u32 = 0x4755524c; // 'GURL' | ||
| 67 | const DIRECT_OBJECT_KEY: u32 = 0x2d2d2d2d; // '----' | ||
| 65 | static mut APP_CLASS: *const Class = ptr::null(); | 68 | static mut APP_CLASS: *const Class = ptr::null(); |
| 66 | static mut APP_DELEGATE_CLASS: *const Class = ptr::null(); | 69 | static mut APP_DELEGATE_CLASS: *const Class = ptr::null(); |
| 67 | 70 | ||
| @@ -136,8 +139,8 @@ unsafe fn build_classes() { · 68 unmodified lines | |||
| 136 | handle_dock_menu as extern "C" fn(&mut Object, Sel, id) -> id, | 139 | handle_dock_menu as extern "C" fn(&mut Object, Sel, id) -> id, |
| 137 | ); | 140 | ); |
| 138 | decl.add_method( | 141 | decl.add_method( |
| 139 | sel!(application:openURLs:), | 142 | sel!(handleGetURLEvent:withReplyEvent:), |
| 140 | open_urls as extern "C" fn(&mut Object, Sel, id, id), | 143 | handle_get_url_event as extern "C" fn(&mut Object, Sel, id, id), |
| 141 | ); | 144 | ); |
| 142 | 145 | ||
| 143 | decl.add_method( | 146 | decl.add_method( |
| @@ -1178,7 +1181,7 @@ unsafe fn get_mac_platform(object: &mut Object) -> &MacPlatform { · 1034 unmodified lines | |||
| 1178 | } | 1181 | } |
| 1179 | } | 1182 | } |
| 1180 | 1183 | ||
| 1181 | extern "C" fn will_finish_launching(_this: &mut Object, _: Sel, _: id) { | 1184 | extern "C" fn will_finish_launching(this: &mut Object, _: Sel, _: id) { |
| 1182 | unsafe { | 1185 | unsafe { |
| 1183 | let user_defaults: id = msg_send![class!(NSUserDefaults), standardUserDefaults]; | 1186 | let user_defaults: id = msg_send![class!(NSUserDefaults), standardUserDefaults]; |
| 1184 | 1187 | ||
| @@ -1192,6 +1195,17 @@ extern "C" fn will_finish_launching(_this: &mut Object, _: Sel, _: id) { · 7 unmodified lines | |||
| 1192 | let false_value: id = msg_send![class!(NSNumber), numberWithBool:false]; | 1195 | let false_value: id = msg_send![class!(NSNumber), numberWithBool:false]; |
| 1193 | let _: () = msg_send![user_defaults, setObject: false_value forKey: name]; | 1196 | let _: () = msg_send![user_defaults, setObject: false_value forKey: name]; |
| 1194 | } | 1197 | } |
| 1198 | |||
| 1199 | // Register kAEGetURL Apple Event handler so that URL open requests are | ||
| 1200 | // delivered synchronously before applicationDidFinishLaunching:, replacing | ||
| 1201 | // application:openURLs: which has non-deterministic timing. | ||
| 1202 | let event_manager: id = msg_send![class!(NSAppleEventManager), sharedAppleEventManager]; | ||
| 1203 | let _: () = msg_send![event_manager, | ||
| 1204 | setEventHandler: this as id | ||
| 1205 | andSelector: sel!(handleGetURLEvent:withReplyEvent:) | ||
| 1206 | forEventClass: INTERNET_EVENT_CLASS | ||
| 1207 | andEventID: AE_GET_URL | ||
| 1208 | ]; | ||
| 1195 | } | 1209 | } |
| 1196 | } | 1210 | } |
| 1197 | 1211 | ||
| @@ -1288,27 +1302,36 @@ extern "C" fn on_thermal_state_change(this: &mut Object, _: Sel, _: id) { · 90 unmodified lines | |||
| 1288 | } | 1302 | } |
| 1289 | } | 1303 | } |
| 1290 | 1304 | ||
| 1291 | extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { | 1305 | extern "C" fn handle_get_url_event(this: &mut Object, _: Sel, event: id, _reply: id) { |
| 1292 | let urls = unsafe { | 1306 | unsafe { |
| 1293 | (0..urls.count()) | 1307 | let descriptor: id = msg_send![event, paramDescriptorForKeyword: DIRECT_OBJECT_KEY]; |
| 1294 | .filter_map(|i| { | 1308 | if descriptor == nil { |
| 1295 | let url = urls.objectAtIndex(i); | 1309 | return; |
| 1296 | match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() { | 1310 | } |
| 1297 | Ok(string) => Some(string.to_string()), | 1311 | let url_string: id = msg_send![descriptor, stringValue]; |
| 1298 | Err(err) => { | 1312 | if url_string == nil { |
| 1299 | log::error!("error converting path to string: {}", err); | 1313 | return; |
| 1300 | None | 1314 | } |
| 1301 | } | 1315 | let utf8_ptr = url_string.UTF8String() as *mut c_char; |
| 1316 | if utf8_ptr.is_null() { | ||
| 1317 | return; | ||
| 1318 | } | ||
| 1319 | let url_str = CStr::from_ptr(utf8_ptr); | ||
| 1320 | match url_str.to_str() { | ||
| 1321 | Ok(string) => { | ||
| 1322 | let urls = vec![string.to_string()]; | ||
| 1323 | let platform = get_mac_platform(this); | ||
| 1324 | let mut lock = platform.0.lock(); | ||
| 1325 | if let Some(mut callback) = lock.open_urls.take() { | ||
| 1326 | drop(lock); | ||
| 1327 | callback(urls); | ||
| 1328 | platform.0.lock().open_urls.get_or_insert(callback); | ||
| 1302 | } | 1329 | } |
| 1303 | }) | 1330 | } |
| 1304 | .collect::<Vec<_>>() | 1331 | Err(err) => { |
| 1305 | }; | 1332 | log::error!("error converting URL to string: {}", err); |
| 1306 | let platform = unsafe { get_mac_platform(this) }; | 1333 | } |
| 1307 | let mut lock = platform.0.lock(); | 1334 | } |
| 1308 | if let Some(mut callback) = lock.open_urls.take() { | ||
| 1309 | drop(lock); | ||
| 1310 | callback(urls); | ||
| 1311 | platform.0.lock().open_urls.get_or_insert(callback); | ||
| 1312 | } | 1335 | } |
| 1313 | } | 1336 | } |
| 1314 | 1337 | ||