workspace: Do not reuse window for sub directory (only for root directory and sub files) (#24560)

smit created

Closes #10232

Context:

We have three ways to open files or dirs in Zed: `zed`, `zed --new`, and
`zed --add`. `--new` forces the project to open in a new window, while
`--add` forces it to open in an existing window (even if the dir isnโ€™t a
subdir of an existing project or the file isnโ€™t part of it).

Using just `zed` tries to open it in an existing window based on similar
logic of `--add`, but if no related project is found the dir, opens in a
new window.

Problem:

Right now, subdirs that are part of an existing project open in the
existing window when using `zed`. By default, subdirs should open in a
new window instead. If someone wants to open it in the existing window,
they can explicitly use `--add`. After this PR, only root dir and files
will focus on existing window, when `zed ` is used.

Fix:

For the `zed` case, weโ€™ve filtered out subdirs in the logic that assigns
them to an existing window.

Release Notes:

- Fixed an issue where subdirectories of an already opened project, when
opened via the terminal, would open in the existing project instead of a
new window.

Change summary

crates/project/src/project.rs     | 34 ++++++++--
crates/workspace/src/workspace.rs | 96 +++++++++++++++++---------------
crates/worktree/src/worktree.rs   |  5 -
3 files changed, 77 insertions(+), 58 deletions(-)

Detailed changes

crates/project/src/project.rs ๐Ÿ”—

@@ -95,7 +95,10 @@ use task_store::TaskStore;
 use terminals::Terminals;
 use text::{Anchor, BufferId};
 use toolchain_store::EmptyToolchainStore;
-use util::{paths::compare_paths, ResultExt as _};
+use util::{
+    paths::{compare_paths, SanitizedPath},
+    ResultExt as _,
+};
 use worktree::{CreatedEntry, Snapshot, Traversal};
 use worktree_store::{WorktreeStore, WorktreeStoreEvent};
 
@@ -1484,22 +1487,37 @@ impl Project {
             .and_then(|worktree| worktree.read(cx).status_for_file(&project_path.path))
     }
 
-    pub fn visibility_for_paths(&self, paths: &[PathBuf], cx: &App) -> Option<bool> {
+    pub fn visibility_for_paths(
+        &self,
+        paths: &[PathBuf],
+        metadatas: &[Metadata],
+        exclude_sub_dirs: bool,
+        cx: &App,
+    ) -> Option<bool> {
         paths
             .iter()
-            .map(|path| self.visibility_for_path(path, cx))
+            .zip(metadatas)
+            .map(|(path, metadata)| self.visibility_for_path(path, metadata, exclude_sub_dirs, cx))
             .max()
             .flatten()
     }
 
-    pub fn visibility_for_path(&self, path: &Path, cx: &App) -> Option<bool> {
+    pub fn visibility_for_path(
+        &self,
+        path: &Path,
+        metadata: &Metadata,
+        exclude_sub_dirs: bool,
+        cx: &App,
+    ) -> Option<bool> {
+        let sanitized_path = SanitizedPath::from(path);
+        let path = sanitized_path.as_path();
         self.worktrees(cx)
             .filter_map(|worktree| {
                 let worktree = worktree.read(cx);
-                worktree
-                    .as_local()?
-                    .contains_abs_path(path)
-                    .then(|| worktree.is_visible())
+                let abs_path = worktree.as_local()?.abs_path();
+                let contains = path == abs_path
+                    || (path.starts_with(abs_path) && (!exclude_sub_dirs || !metadata.is_dir));
+                contains.then(|| worktree.is_visible())
             })
             .max()
     }

crates/workspace/src/workspace.rs ๐Ÿ”—

@@ -5958,7 +5958,6 @@ pub struct OpenOptions {
     pub replace_window: Option<WindowHandle<Workspace>>,
     pub env: Option<HashMap<String, String>>,
 }
-
 #[allow(clippy::type_complexity)]
 pub fn open_paths(
     abs_paths: &[PathBuf],
@@ -5976,58 +5975,65 @@ pub fn open_paths(
     let mut best_match = None;
     let mut open_visible = OpenVisible::All;
 
-    if open_options.open_new_workspace != Some(true) {
-        for window in local_workspace_windows(cx) {
-            if let Ok(workspace) = window.read(cx) {
-                let m = workspace
-                    .project
-                    .read(cx)
-                    .visibility_for_paths(&abs_paths, cx);
-                if m > best_match {
-                    existing = Some(window);
-                    best_match = m;
-                } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
-                    existing = Some(window)
-                }
-            }
-        }
-    }
-
     cx.spawn(move |mut cx| async move {
-        if open_options.open_new_workspace.is_none() && existing.is_none() {
-            let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
-            if futures::future::join_all(all_files)
+        if open_options.open_new_workspace != Some(true) {
+            let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
+            let all_metadatas = futures::future::join_all(all_paths)
                 .await
                 .into_iter()
                 .filter_map(|result| result.ok().flatten())
-                .all(|file| !file.is_dir)
-            {
-                cx.update(|cx| {
-                    if let Some(window) = cx
-                        .active_window()
-                        .and_then(|window| window.downcast::<Workspace>())
-                    {
-                        if let Ok(workspace) = window.read(cx) {
-                            let project = workspace.project().read(cx);
-                            if project.is_local() && !project.is_via_collab() {
-                                existing = Some(window);
-                                open_visible = OpenVisible::None;
-                                return;
-                            }
+                .collect::<Vec<_>>();
+
+            cx.update(|cx| {
+                for window in local_workspace_windows(&cx) {
+                    if let Ok(workspace) = window.read(&cx) {
+                        let m = workspace.project.read(&cx).visibility_for_paths(
+                            &abs_paths,
+                            &all_metadatas,
+                            open_options.open_new_workspace == None,
+                            cx,
+                        );
+                        if m > best_match {
+                            existing = Some(window);
+                            best_match = m;
+                        } else if best_match.is_none()
+                            && open_options.open_new_workspace == Some(false)
+                        {
+                            existing = Some(window)
                         }
                     }
-                    for window in local_workspace_windows(cx) {
-                        if let Ok(workspace) = window.read(cx) {
-                            let project = workspace.project().read(cx);
-                            if project.is_via_collab() {
-                                continue;
+                }
+            })?;
+
+            if open_options.open_new_workspace.is_none() && existing.is_none() {
+                if all_metadatas.iter().all(|file| !file.is_dir) {
+                    cx.update(|cx| {
+                        if let Some(window) = cx
+                            .active_window()
+                            .and_then(|window| window.downcast::<Workspace>())
+                        {
+                            if let Ok(workspace) = window.read(cx) {
+                                let project = workspace.project().read(cx);
+                                if project.is_local() && !project.is_via_collab() {
+                                    existing = Some(window);
+                                    open_visible = OpenVisible::None;
+                                    return;
+                                }
                             }
-                            existing = Some(window);
-                            open_visible = OpenVisible::None;
-                            break;
                         }
-                    }
-                })?;
+                        for window in local_workspace_windows(cx) {
+                            if let Ok(workspace) = window.read(cx) {
+                                let project = workspace.project().read(cx);
+                                if project.is_via_collab() {
+                                    continue;
+                                }
+                                existing = Some(window);
+                                open_visible = OpenVisible::None;
+                                break;
+                            }
+                        }
+                    })?;
+                }
             }
         }
 

crates/worktree/src/worktree.rs ๐Ÿ”—

@@ -1335,11 +1335,6 @@ impl LocalWorktree {
         &self.fs
     }
 
-    pub fn contains_abs_path(&self, path: &Path) -> bool {
-        let path = SanitizedPath::from(path);
-        path.starts_with(&self.abs_path)
-    }
-
     pub fn is_path_private(&self, path: &Path) -> bool {
         !self.share_private_files && self.settings.is_path_private(path)
     }