Implement `zed --wait`

Antonio Scandurra created

Change summary

crates/cli/src/cli.rs             |  1 
crates/cli/src/main.rs            |  3 
crates/gpui/src/app.rs            |  8 +-
crates/journal/src/journal.rs     |  2 
crates/workspace/src/workspace.rs | 80 +++++++++++++++++++-------------
crates/zed/src/main.rs            | 80 ++++++++++++++++++++++++++++++--
6 files changed, 131 insertions(+), 43 deletions(-)

Detailed changes

crates/cli/src/cli.rs 🔗

@@ -15,6 +15,7 @@ pub enum CliRequest {
 
 #[derive(Debug, Serialize, Deserialize)]
 pub enum CliResponse {
+    Ping,
     Stdout { message: String },
     Stderr { message: String },
     Exit { status: i32 },

crates/cli/src/main.rs 🔗

@@ -34,11 +34,12 @@ fn main() -> Result<()> {
             .into_iter()
             .map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error)))
             .collect::<Result<Vec<PathBuf>>>()?,
-        wait: false,
+        wait: args.wait,
     })?;
 
     while let Ok(response) = rx.recv() {
         match response {
+            CliResponse::Ping => {}
             CliResponse::Stdout { message } => println!("{message}"),
             CliResponse::Stderr { message } => eprintln!("{message}"),
             CliResponse::Exit { status } => std::process::exit(status),

crates/gpui/src/app.rs 🔗

@@ -733,7 +733,7 @@ type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext
 type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 type FocusObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 type GlobalObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
-type ReleaseObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
+type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
 type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
 
 pub struct MutableAppContext {
@@ -1259,12 +1259,12 @@ impl MutableAppContext {
         }
     }
 
-    pub fn observe_release<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
+    pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription
     where
         E: Entity,
         E::Event: 'static,
         H: Handle<E>,
-        F: 'static + FnMut(&E, &mut Self),
+        F: 'static + FnOnce(&E, &mut Self),
     {
         let id = post_inc(&mut self.next_subscription_id);
         self.release_observations
@@ -2211,7 +2211,7 @@ impl MutableAppContext {
     fn handle_entity_release_effect(&mut self, entity_id: usize, entity: &dyn Any) {
         let callbacks = self.release_observations.lock().remove(&entity_id);
         if let Some(callbacks) = callbacks {
-            for (_, mut callback) in callbacks {
+            for (_, callback) in callbacks {
                 callback(entity, self);
             }
         }

crates/journal/src/journal.rs 🔗

@@ -43,7 +43,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     cx.spawn(|mut cx| {
         async move {
             let (journal_dir, entry_path) = create_entry.await?;
-            let workspace = cx
+            let (workspace, _) = cx
                 .update(|cx| workspace::open_paths(&[journal_dir], &app_state, cx))
                 .await;
 

crates/workspace/src/workspace.rs 🔗

@@ -376,6 +376,11 @@ pub trait ItemHandle: 'static + fmt::Debug {
         -> Task<Result<()>>;
     fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
     fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
+    fn on_release(
+        &self,
+        cx: &mut MutableAppContext,
+        callback: Box<dyn FnOnce(&mut MutableAppContext)>,
+    ) -> gpui::Subscription;
 }
 
 pub trait WeakItemHandle {
@@ -411,17 +416,17 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         Box::new(self.clone())
     }
 
-    fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
+    fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
         self.update(cx, |item, cx| {
-            cx.add_option_view(|cx| item.clone_on_split(cx))
+            item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
         })
-        .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
     }
 
-    fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
+    fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
         self.update(cx, |item, cx| {
-            item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
+            cx.add_option_view(|cx| item.clone_on_split(cx))
         })
+        .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
     }
 
     fn added_to_pane(
@@ -512,6 +517,30 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         self.update(cx, |this, cx| this.navigate(data, cx))
     }
 
+    fn id(&self) -> usize {
+        self.id()
+    }
+
+    fn to_any(&self) -> AnyViewHandle {
+        self.into()
+    }
+
+    fn is_dirty(&self, cx: &AppContext) -> bool {
+        self.read(cx).is_dirty(cx)
+    }
+
+    fn has_conflict(&self, cx: &AppContext) -> bool {
+        self.read(cx).has_conflict(cx)
+    }
+
+    fn can_save(&self, cx: &AppContext) -> bool {
+        self.read(cx).can_save(cx)
+    }
+
+    fn can_save_as(&self, cx: &AppContext) -> bool {
+        self.read(cx).can_save_as(cx)
+    }
+
     fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
         self.update(cx, |item, cx| item.save(project, cx))
     }
@@ -533,30 +562,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         self.update(cx, |item, cx| item.reload(project, cx))
     }
 
-    fn is_dirty(&self, cx: &AppContext) -> bool {
-        self.read(cx).is_dirty(cx)
-    }
-
-    fn has_conflict(&self, cx: &AppContext) -> bool {
-        self.read(cx).has_conflict(cx)
-    }
-
-    fn id(&self) -> usize {
-        self.id()
-    }
-
-    fn to_any(&self) -> AnyViewHandle {
-        self.into()
-    }
-
-    fn can_save(&self, cx: &AppContext) -> bool {
-        self.read(cx).can_save(cx)
-    }
-
-    fn can_save_as(&self, cx: &AppContext) -> bool {
-        self.read(cx).can_save_as(cx)
-    }
-
     fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
         self.read(cx).act_as_type(type_id, self, cx)
     }
@@ -570,6 +575,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
             None
         }
     }
+
+    fn on_release(
+        &self,
+        cx: &mut MutableAppContext,
+        callback: Box<dyn FnOnce(&mut MutableAppContext)>,
+    ) -> gpui::Subscription {
+        cx.observe_release(self, move |_, cx| callback(cx))
+    }
 }
 
 impl Into<AnyViewHandle> for Box<dyn ItemHandle> {
@@ -2102,7 +2115,10 @@ pub fn open_paths(
     abs_paths: &[PathBuf],
     app_state: &Arc<AppState>,
     cx: &mut MutableAppContext,
-) -> Task<ViewHandle<Workspace>> {
+) -> Task<(
+    ViewHandle<Workspace>,
+    Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
+)> {
     log::info!("open paths {:?}", abs_paths);
 
     // Open paths in existing workspace if possible
@@ -2139,8 +2155,8 @@ pub fn open_paths(
 
     let task = workspace.update(cx, |workspace, cx| workspace.open_paths(abs_paths, cx));
     cx.spawn(|_| async move {
-        task.await;
-        workspace
+        let items = task.await;
+        (workspace, items)
     })
 }
 

crates/zed/src/main.rs 🔗

@@ -11,7 +11,7 @@ use client::{self, http, ChannelList, UserStore};
 use fs::OpenOptions;
 use futures::{
     channel::{mpsc, oneshot},
-    SinkExt, StreamExt,
+    FutureExt, SinkExt, StreamExt,
 };
 use gpui::{App, AssetSource, AsyncAppContext, Task};
 use log::LevelFilter;
@@ -19,7 +19,7 @@ use parking_lot::Mutex;
 use project::Fs;
 use settings::{self, KeymapFile, Settings, SettingsFileContent};
 use smol::process::Command;
-use std::{env, fs, path::PathBuf, sync::Arc, thread};
+use std::{env, fs, path::PathBuf, sync::Arc, thread, time::Duration};
 use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
 use util::ResultExt;
 use workspace::{self, AppState, OpenNew, OpenPaths};
@@ -360,9 +360,79 @@ async fn handle_cli_connection(
 ) {
     if let Some(request) = requests.next().await {
         match request {
-            CliRequest::Open { paths, .. } => {
-                cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths, app_state }));
-                responses.send(CliResponse::Exit { status: 0 }).log_err();
+            CliRequest::Open { paths, wait } => {
+                let (workspace, items) = cx
+                    .update(|cx| workspace::open_paths(&paths, &app_state, cx))
+                    .await;
+
+                let mut errored = false;
+                let mut futures = Vec::new();
+                cx.update(|cx| {
+                    for (item, path) in items.into_iter().zip(&paths) {
+                        match item {
+                            Some(Ok(item)) => {
+                                let released = oneshot::channel();
+                                item.on_release(
+                                    cx,
+                                    Box::new(move |_| {
+                                        let _ = released.0.send(());
+                                    }),
+                                )
+                                .detach();
+                                futures.push(released.1);
+                            }
+                            Some(Err(err)) => {
+                                responses
+                                    .send(CliResponse::Stderr {
+                                        message: format!("error opening {:?}: {}", path, err),
+                                    })
+                                    .log_err();
+                                errored = true;
+                            }
+                            None => {}
+                        }
+                    }
+                });
+
+                if wait {
+                    let background = cx.background();
+                    let wait = async move {
+                        if paths.is_empty() {
+                            let (done_tx, done_rx) = oneshot::channel();
+                            let _subscription = cx.update(|cx| {
+                                cx.observe_release(&workspace, move |_, _| {
+                                    let _ = done_tx.send(());
+                                })
+                            });
+                            drop(workspace);
+                            let _ = done_rx.await;
+                        } else {
+                            let _ = futures::future::try_join_all(futures).await;
+                        };
+                    }
+                    .fuse();
+                    futures::pin_mut!(wait);
+
+                    loop {
+                        // Repeatedly check if CLI is still open to avoid wasting resources
+                        // waiting for files or workspaces to close.
+                        let mut timer = background.timer(Duration::from_secs(1)).fuse();
+                        futures::select_biased! {
+                            _ = wait => break,
+                            _ = timer => {
+                                if responses.send(CliResponse::Ping).is_err() {
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+
+                responses
+                    .send(CliResponse::Exit {
+                        status: if errored { 1 } else { 0 },
+                    })
+                    .log_err();
             }
         }
     }