Add a command for installing the CLI

Max Brunsfeld created

Change summary

crates/gpui/src/platform.rs              |  3 
crates/gpui/src/platform/mac/platform.rs | 66 ++++++++++++-------------
crates/gpui/src/platform/test.rs         |  2 
crates/zed/src/zed.rs                    | 17 ++++++
4 files changed, 49 insertions(+), 39 deletions(-)

Detailed changes

crates/gpui/src/platform.rs 🔗

@@ -54,8 +54,7 @@ pub trait Platform: Send + Sync {
     fn set_cursor_style(&self, style: CursorStyle);
 
     fn local_timezone(&self) -> UtcOffset;
-
-    fn path_for_resource(&self, name: Option<&str>, extension: Option<&str>) -> Result<PathBuf>;
+    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
 }
 
 pub(crate) trait ForegroundPlatform {

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -14,9 +14,7 @@ use cocoa::{
         NSPasteboardTypeString, NSSavePanel, NSWindow,
     },
     base::{id, nil, selector, YES},
-    foundation::{
-        NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSString, NSUInteger, NSURL,
-    },
+    foundation::{NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSString, NSURL},
 };
 use core_foundation::{
     base::{CFType, CFTypeRef, OSStatus, TCFType as _},
@@ -38,8 +36,8 @@ use ptr::null_mut;
 use std::{
     cell::{Cell, RefCell},
     convert::TryInto,
-    ffi::{c_void, CStr},
-    os::raw::c_char,
+    ffi::{c_void, CStr, OsStr},
+    os::{raw::c_char, unix::ffi::OsStrExt},
     path::{Path, PathBuf},
     ptr,
     rc::Rc,
@@ -48,9 +46,6 @@ use std::{
 };
 use time::UtcOffset;
 
-#[allow(non_upper_case_globals)]
-const NSUTF8StringEncoding: NSUInteger = 4;
-
 const MAC_PLATFORM_IVAR: &'static str = "platform";
 static mut APP_CLASS: *const Class = ptr::null();
 static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
@@ -274,10 +269,9 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
                     for i in 0..urls.count() {
                         let url = urls.objectAtIndex(i);
                         if url.isFileURL() == YES {
-                            let path = std::ffi::CStr::from_ptr(url.path().UTF8String())
-                                .to_string_lossy()
-                                .to_string();
-                            result.push(PathBuf::from(path));
+                            if let Ok(path) = ns_url_to_path(url) {
+                                result.push(path)
+                            }
                         }
                     }
                     Some(result)
@@ -305,19 +299,13 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
             let (done_tx, done_rx) = oneshot::channel();
             let done_tx = Cell::new(Some(done_tx));
             let block = ConcreteBlock::new(move |response: NSModalResponse| {
-                let result = if response == NSModalResponse::NSModalResponseOk {
+                let mut result = None;
+                if response == NSModalResponse::NSModalResponseOk {
                     let url = panel.URL();
                     if url.isFileURL() == YES {
-                        let path = std::ffi::CStr::from_ptr(url.path().UTF8String())
-                            .to_string_lossy()
-                            .to_string();
-                        Some(PathBuf::from(path))
-                    } else {
-                        None
+                        result = ns_url_to_path(panel.URL()).ok()
                     }
-                } else {
-                    None
-                };
+                }
 
                 if let Some(mut done_tx) = done_tx.take() {
                     let _ = postage::sink::Sink::try_send(&mut done_tx, result);
@@ -612,22 +600,18 @@ impl platform::Platform for MacPlatform {
         }
     }
 
-    fn path_for_resource(&self, name: Option<&str>, extension: Option<&str>) -> Result<PathBuf> {
+    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
         unsafe {
             let bundle: id = NSBundle::mainBundle();
             if bundle.is_null() {
                 Err(anyhow!("app is not running inside a bundle"))
             } else {
-                let name = name.map_or(nil, |name| ns_string(name));
-                let extension = extension.map_or(nil, |extension| ns_string(extension));
-                let path: id = msg_send![bundle, pathForResource: name ofType: extension];
-                if path.is_null() {
-                    Err(anyhow!("resource could not be found"))
+                let name = ns_string(name);
+                let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name];
+                if url.is_null() {
+                    Err(anyhow!("resource not found"))
                 } else {
-                    let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
-                    let bytes = path.UTF8String() as *const u8;
-                    let path = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap();
-                    Ok(PathBuf::from(path))
+                    ns_url_to_path(url)
                 }
             }
         }
@@ -717,9 +701,7 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
             .into_iter()
             .filter_map(|i| {
                 let path = urls.objectAtIndex(i);
-                match dbg!(
-                    CStr::from_ptr(path.absoluteString().UTF8String() as *mut c_char).to_str()
-                ) {
+                match CStr::from_ptr(path.absoluteString().UTF8String() as *mut c_char).to_str() {
                     Ok(string) => Some(string.to_string()),
                     Err(err) => {
                         log::error!("error converting path to string: {}", err);
@@ -754,6 +736,20 @@ unsafe fn ns_string(string: &str) -> id {
     NSString::alloc(nil).init_str(string).autorelease()
 }
 
+unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
+    let path: *mut c_char = msg_send![url, fileSystemRepresentation];
+    if path.is_null() {
+        Err(anyhow!(
+            "url is not a file path: {}",
+            CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy()
+        ))
+    } else {
+        Ok(PathBuf::from(OsStr::from_bytes(
+            CStr::from_ptr(path).to_bytes(),
+        )))
+    }
+}
+
 mod security {
     #![allow(non_upper_case_globals)]
     use super::*;

crates/gpui/src/platform/test.rs 🔗

@@ -163,7 +163,7 @@ impl super::Platform for Platform {
         UtcOffset::UTC
     }
 
-    fn path_for_resource(&self, _name: Option<&str>, _extension: Option<&str>) -> Result<PathBuf> {
+    fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
         Err(anyhow!("app not running inside a bundle"))
     }
 }

crates/zed/src/zed.rs 🔗

@@ -38,7 +38,8 @@ actions!(
         DebugElements,
         OpenSettings,
         IncreaseBufferFontSize,
-        DecreaseBufferFontSize
+        DecreaseBufferFontSize,
+        InstallCommandLineTool,
     ]
 );
 
@@ -66,6 +67,20 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
             cx.refresh_windows();
         });
     });
+    cx.add_global_action(move |_: &InstallCommandLineTool, cx| {
+        cx.spawn(|cx| async move {
+            let path = cx.platform().path_for_auxiliary_executable("cli")?;
+            let link_path = "/usr/local/bin/zed";
+            smol::fs::unix::symlink(link_path, path.as_path()).await?;
+            log::info!(
+                "created symlink {} -> {}",
+                link_path,
+                path.to_string_lossy()
+            );
+            Ok::<_, anyhow::Error>(())
+        })
+        .detach();
+    });
     cx.add_action({
         let app_state = app_state.clone();
         move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {