Merge pull request #884 from zed-industries/show-project-browser-when-opening-folder

Antonio Scandurra created

Show project browser when opening folder

Change summary

crates/file_finder/src/file_finder.rs |   2 
crates/gpui/src/app.rs                |  12 ++
crates/journal/src/journal.rs         |   2 
crates/workspace/src/sidebar.rs       |  21 ++--
crates/workspace/src/workspace.rs     | 121 +++++++++++++++++-----------
crates/zed/src/main.rs                |   6 
crates/zed/src/zed.rs                 |  58 ++++++++++++-
7 files changed, 153 insertions(+), 69 deletions(-)

Detailed changes

crates/file_finder/src/file_finder.rs 🔗

@@ -469,7 +469,7 @@ mod tests {
         workspace
             .update(cx, |workspace, cx| {
                 workspace.open_paths(
-                    &[PathBuf::from("/root/dir1"), PathBuf::from("/root/dir2")],
+                    vec![PathBuf::from("/root/dir1"), PathBuf::from("/root/dir2")],
                     cx,
                 )
             })

crates/gpui/src/app.rs 🔗

@@ -566,6 +566,18 @@ impl AsyncAppContext {
         self.update(|cx| cx.add_view(window_id, build_view))
     }
 
+    pub fn add_window<T, F>(
+        &mut self,
+        window_options: WindowOptions,
+        build_root_view: F,
+    ) -> (usize, ViewHandle<T>)
+    where
+        T: View,
+        F: FnOnce(&mut ViewContext<T>) -> T,
+    {
+        self.update(|cx| cx.add_window(window_options, build_root_view))
+    }
+
     pub fn platform(&self) -> Arc<dyn Platform> {
         self.0.borrow().platform()
     }

crates/journal/src/journal.rs 🔗

@@ -49,7 +49,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
 
             let opened = workspace
                 .update(&mut cx, |workspace, cx| {
-                    workspace.open_paths(&[entry_path], cx)
+                    workspace.open_paths(vec![entry_path], cx)
                 })
                 .await;
 

crates/workspace/src/sidebar.rs 🔗

@@ -8,7 +8,8 @@ pub struct Sidebar {
     side: Side,
     items: Vec<Item>,
     active_item_ix: Option<usize>,
-    width: Rc<RefCell<f32>>,
+    actual_width: Rc<RefCell<f32>>,
+    custom_width: Rc<RefCell<f32>>,
 }
 
 #[derive(Clone, Copy, Deserialize)]
@@ -42,7 +43,8 @@ impl Sidebar {
             side,
             items: Default::default(),
             active_item_ix: None,
-            width: Rc::new(RefCell::new(260.)),
+            actual_width: Rc::new(RefCell::new(260.)),
+            custom_width: Rc::new(RefCell::new(260.)),
         }
     }
 
@@ -137,12 +139,12 @@ impl Sidebar {
             container.add_child(
                 Hook::new(
                     ConstrainedBox::new(ChildView::new(active_item).boxed())
-                        .with_max_width(*self.width.borrow())
+                        .with_max_width(*self.custom_width.borrow())
                         .boxed(),
                 )
                 .on_after_layout({
-                    let width = self.width.clone();
-                    move |size, _| *width.borrow_mut() = size.x()
+                    let actual_width = self.actual_width.clone();
+                    move |size, _| *actual_width.borrow_mut() = size.x()
                 })
                 .flex(1., false)
                 .boxed(),
@@ -157,7 +159,8 @@ impl Sidebar {
     }
 
     fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Workspace>) -> ElementBox {
-        let width = self.width.clone();
+        let actual_width = self.actual_width.clone();
+        let custom_width = self.custom_width.clone();
         let side = self.side;
         MouseEventHandler::new::<Self, _, _>(side as usize, cx, |_, _| {
             Container::new(Empty::new().boxed())
@@ -171,10 +174,10 @@ impl Sidebar {
         })
         .with_cursor_style(CursorStyle::ResizeLeftRight)
         .on_drag(move |delta, cx| {
-            let prev_width = *width.borrow();
+            let prev_width = *actual_width.borrow();
             match side {
-                Side::Left => *width.borrow_mut() = 0f32.max(prev_width + delta.x()),
-                Side::Right => *width.borrow_mut() = 0f32.max(prev_width - delta.x()),
+                Side::Left => *custom_width.borrow_mut() = 0f32.max(prev_width + delta.x()),
+                Side::Right => *custom_width.borrow_mut() = 0f32.max(prev_width - delta.x()),
             }
 
             cx.notify();

crates/workspace/src/workspace.rs 🔗

@@ -31,7 +31,7 @@ pub use pane_group::*;
 use postage::prelude::Stream;
 use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree};
 use settings::Settings;
-use sidebar::{Side, Sidebar, ToggleSidebarItem, ToggleSidebarItemFocus};
+use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use std::{
@@ -859,44 +859,49 @@ impl Workspace {
 
     pub fn open_paths(
         &mut self,
-        abs_paths: &[PathBuf],
+        mut abs_paths: Vec<PathBuf>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>> {
-        let entries = abs_paths
-            .iter()
-            .cloned()
-            .map(|path| self.project_path_for_path(&path, cx))
-            .collect::<Vec<_>>();
-
         let fs = self.fs.clone();
-        let tasks = abs_paths
-            .iter()
-            .cloned()
-            .zip(entries.into_iter())
-            .map(|(abs_path, project_path)| {
-                cx.spawn(|this, mut cx| {
-                    let fs = fs.clone();
-                    async move {
-                        let project_path = project_path.await.ok()?;
-                        if fs.is_file(&abs_path).await {
-                            Some(
-                                this.update(&mut cx, |this, cx| this.open_path(project_path, cx))
+
+        // Sort the paths to ensure we add worktrees for parents before their children.
+        abs_paths.sort_unstable();
+        cx.spawn(|this, mut cx| async move {
+            let mut entries = Vec::new();
+            for path in &abs_paths {
+                entries.push(
+                    this.update(&mut cx, |this, cx| this.project_path_for_path(path, cx))
+                        .await
+                        .ok(),
+                );
+            }
+
+            let tasks = abs_paths
+                .iter()
+                .cloned()
+                .zip(entries.into_iter())
+                .map(|(abs_path, project_path)| {
+                    let this = this.clone();
+                    cx.spawn(|mut cx| {
+                        let fs = fs.clone();
+                        async move {
+                            let project_path = project_path?;
+                            if fs.is_file(&abs_path).await {
+                                Some(
+                                    this.update(&mut cx, |this, cx| {
+                                        this.open_path(project_path, cx)
+                                    })
                                     .await,
-                            )
-                        } else {
-                            None
+                                )
+                            } else {
+                                None
+                            }
                         }
-                    }
+                    })
                 })
-            })
-            .collect::<Vec<_>>();
+                .collect::<Vec<_>>();
 
-        cx.foreground().spawn(async move {
-            let mut items = Vec::new();
-            for task in tasks {
-                items.push(task.await);
-            }
-            items
+            futures::future::join_all(tasks).await
         })
     }
 
@@ -2136,23 +2141,43 @@ pub fn open_paths(
         }
     }
 
-    let workspace = existing.unwrap_or_else(|| {
-        cx.add_window((app_state.build_window_options)(), |cx| {
-            let project = Project::local(
-                app_state.client.clone(),
-                app_state.user_store.clone(),
-                app_state.languages.clone(),
-                app_state.fs.clone(),
-                cx,
-            );
-            (app_state.build_workspace)(project, &app_state, cx)
-        })
-        .1
-    });
+    let app_state = app_state.clone();
+    let abs_paths = abs_paths.to_vec();
+    cx.spawn(|mut cx| async move {
+        let workspace = if let Some(existing) = existing {
+            existing
+        } else {
+            let contains_directory =
+                futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
+                    .await
+                    .contains(&false);
+
+            cx.add_window((app_state.build_window_options)(), |cx| {
+                let project = Project::local(
+                    app_state.client.clone(),
+                    app_state.user_store.clone(),
+                    app_state.languages.clone(),
+                    app_state.fs.clone(),
+                    cx,
+                );
+                let mut workspace = (app_state.build_workspace)(project, &app_state, cx);
+                if contains_directory {
+                    workspace.toggle_sidebar_item(
+                        &ToggleSidebarItem(SidebarItemId {
+                            side: Side::Left,
+                            item_index: 0,
+                        }),
+                        cx,
+                    );
+                }
+                workspace
+            })
+            .1
+        };
 
-    let task = workspace.update(cx, |workspace, cx| workspace.open_paths(abs_paths, cx));
-    cx.spawn(|_| async move {
-        let items = task.await;
+        let items = workspace
+            .update(&mut cx, |workspace, cx| workspace.open_paths(abs_paths, cx))
+            .await;
         (workspace, items)
     })
 }

crates/zed/src/main.rs 🔗

@@ -367,7 +367,7 @@ async fn handle_cli_connection(
                     .await;
 
                 let mut errored = false;
-                let mut futures = Vec::new();
+                let mut item_release_futures = Vec::new();
                 cx.update(|cx| {
                     for (item, path) in items.into_iter().zip(&paths) {
                         match item {
@@ -380,7 +380,7 @@ async fn handle_cli_connection(
                                     }),
                                 )
                                 .detach();
-                                futures.push(released.1);
+                                item_release_futures.push(released.1);
                             }
                             Some(Err(err)) => {
                                 responses
@@ -408,7 +408,7 @@ async fn handle_cli_connection(
                             drop(workspace);
                             let _ = done_rx.await;
                         } else {
-                            let _ = futures::future::try_join_all(futures).await;
+                            let _ = futures::future::try_join_all(item_release_futures).await;
                         };
                     }
                     .fuse();

crates/zed/src/zed.rs 🔗

@@ -283,7 +283,7 @@ fn open_config_file(
         workspace
             .update(&mut cx, |workspace, cx| {
                 if workspace.project().read(cx).is_local() {
-                    workspace.open_paths(&[path.to_path_buf()], cx)
+                    workspace.open_paths(vec![path.to_path_buf()], cx)
                 } else {
                     let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
                         let project = Project::local(
@@ -296,7 +296,7 @@ fn open_config_file(
                         (app_state.build_workspace)(project, &app_state, cx)
                     });
                     workspace.update(cx, |workspace, cx| {
-                        workspace.open_paths(&[path.to_path_buf()], cx)
+                        workspace.open_paths(vec![path.to_path_buf()], cx)
                     })
                 }
             })
@@ -360,8 +360,10 @@ mod tests {
             .await;
         assert_eq!(cx.window_ids().len(), 1);
         let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
-        workspace_1.read_with(cx, |workspace, cx| {
-            assert_eq!(workspace.worktrees(cx).count(), 2)
+        workspace_1.update(cx, |workspace, cx| {
+            assert_eq!(workspace.worktrees(cx).count(), 2);
+            assert!(workspace.left_sidebar_mut().active_item().is_some());
+            assert!(workspace.active_pane().is_focused(cx));
         });
 
         cx.update(|cx| {
@@ -536,8 +538,10 @@ mod tests {
         let fs = app_state.fs.as_fake();
         fs.insert_dir("/dir1").await;
         fs.insert_dir("/dir2").await;
+        fs.insert_dir("/dir3").await;
         fs.insert_file("/dir1/a.txt", "".into()).await;
         fs.insert_file("/dir2/b.txt", "".into()).await;
+        fs.insert_file("/dir3/c.txt", "".into()).await;
 
         let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
         let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
@@ -553,7 +557,9 @@ mod tests {
 
         // Open a file within an existing worktree.
         cx.update(|cx| {
-            workspace.update(cx, |view, cx| view.open_paths(&["/dir1/a.txt".into()], cx))
+            workspace.update(cx, |view, cx| {
+                view.open_paths(vec!["/dir1/a.txt".into()], cx)
+            })
         })
         .await;
         cx.read(|cx| {
@@ -575,7 +581,9 @@ mod tests {
 
         // Open a file outside of any existing worktree.
         cx.update(|cx| {
-            workspace.update(cx, |view, cx| view.open_paths(&["/dir2/b.txt".into()], cx))
+            workspace.update(cx, |view, cx| {
+                view.open_paths(vec!["/dir2/b.txt".into()], cx)
+            })
         })
         .await;
         cx.read(|cx| {
@@ -606,6 +614,42 @@ mod tests {
                 "b.txt"
             );
         });
+
+        // Ensure opening a directory and one of its children only adds one worktree.
+        cx.update(|cx| {
+            workspace.update(cx, |view, cx| {
+                view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], cx)
+            })
+        })
+        .await;
+        cx.read(|cx| {
+            let worktree_roots = workspace
+                .read(cx)
+                .worktrees(cx)
+                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+                .collect::<HashSet<_>>();
+            assert_eq!(
+                worktree_roots,
+                vec!["/dir1", "/dir2/b.txt", "/dir3"]
+                    .into_iter()
+                    .map(Path::new)
+                    .collect(),
+            );
+            assert_eq!(
+                workspace
+                    .read(cx)
+                    .active_pane()
+                    .read(cx)
+                    .active_item()
+                    .unwrap()
+                    .to_any()
+                    .downcast::<Editor>()
+                    .unwrap()
+                    .read(cx)
+                    .title(cx),
+                "c.txt"
+            );
+        });
     }
 
     #[gpui::test]
@@ -627,7 +671,7 @@ mod tests {
         // Open a file within an existing worktree.
         cx.update(|cx| {
             workspace.update(cx, |view, cx| {
-                view.open_paths(&[PathBuf::from("/root/a.txt")], cx)
+                view.open_paths(vec![PathBuf::from("/root/a.txt")], cx)
             })
         })
         .await;