diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5ecd607b785419ac44af2553ad15d9ce8c91ff48..942735b7a198fc0a91ec8c49c97fc3f10e6248f9 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3137,6 +3137,12 @@ impl Workspace { let is_remote = self.project.read(cx).is_via_collab(); let has_worktree = self.project.read(cx).worktrees(cx).next().is_some(); let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx)); + let location = self + .project + .read(cx) + .remote_connection_options(cx) + .map(SerializedWorkspaceLocation::Remote) + .unwrap_or(SerializedWorkspaceLocation::Local); let window_to_replace = if replace_current_window { window_handle @@ -3146,8 +3152,56 @@ impl Workspace { window_handle }; let app_state = self.app_state.clone(); + let fs = app_state.fs.clone(); cx.spawn(async move |_, cx| { + let all_paths_are_directories = if replace_current_window { + let metadata = + futures::future::join_all(paths.iter().map(|path| fs.metadata(path))).await; + metadata + .into_iter() + .all(|entry| entry.ok().flatten().is_some_and(|entry| entry.is_dir)) + } else { + false + }; + + if all_paths_are_directories { + let mut normalized_paths = Vec::with_capacity(paths.len()); + for path in &paths { + if let Some(canonical) = fs.canonicalize(path).await.ok() { + normalized_paths.push(canonical); + } else { + normalized_paths.push(path.clone()); + } + } + + if let Some((target_window, workspace)) = + cx.update(|cx| find_exact_existing_workspace(&normalized_paths, &location, cx)) + { + target_window.update(cx, |multi_workspace, window, cx| { + window.activate_window(); + multi_workspace.activate(workspace.clone(), cx); + })?; + return Ok(workspace); + } + + let OpenResult { workspace, .. } = cx + .update(|cx| { + open_paths( + &normalized_paths, + app_state, + OpenOptions { + open_new_workspace: Some(true), + replace_window: window_to_replace, + ..Default::default() + }, + cx, + ) + }) + .await?; + return Ok(workspace); + } + let OpenResult { workspace, .. } = cx .update(|cx| { open_paths( @@ -8779,6 +8833,40 @@ pub fn workspace_windows_for_location( .collect() } +fn find_exact_existing_workspace( + abs_paths: &[PathBuf], + location: &SerializedWorkspaceLocation, + cx: &App, +) -> Option<(WindowHandle, Entity)> { + let requested_paths = PathList::new(abs_paths); + let active_window = cx + .active_window() + .and_then(|window| window.downcast::()); + let mut windows = workspace_windows_for_location(location, cx); + + if let Some(active_window) = active_window { + if let Some(index) = windows.iter().position(|window| *window == active_window) { + let active_window = windows.remove(index); + windows.insert(0, active_window); + } + } + + windows.into_iter().find_map(|window| { + let multi_workspace = window.read(cx).ok()?; + multi_workspace.workspaces().iter().find_map(|workspace| { + match workspace.read(cx).workspace_location(cx) { + WorkspaceLocation::Location(workspace_location, workspace_paths) + if &workspace_location == location + && workspace_paths.paths() == requested_paths.paths() => + { + Some((window, workspace.clone())) + } + _ => None, + } + }) + }) +} + pub async fn find_existing_workspace( abs_paths: &[PathBuf], open_options: &OpenOptions, @@ -10391,6 +10479,110 @@ mod tests { ); } + #[gpui::test] + async fn test_open_workspace_for_paths_keeps_overlapping_directory_workspaces_distinct( + cx: &mut TestAppContext, + ) { + init_test(cx); + cx.update(|cx| { + use feature_flags::FeatureFlagAppExt as _; + cx.update_flags(false, vec!["agent-v2".to_string()]); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/zed", json!({ "src": {} })).await; + fs.insert_tree("/foo", json!({ "lib": {} })).await; + + let project = Project::test(fs.clone(), ["/zed".as_ref()], cx).await; + let multi_workspace_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + cx.run_until_parked(); + + let open_task = multi_workspace_handle + .update(cx, |multi_workspace, window, cx| { + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| { + workspace.open_workspace_for_paths( + true, + vec![PathBuf::from("/zed"), PathBuf::from("/foo")], + window, + cx, + ) + }) + }) + .unwrap(); + + let opened_workspace = open_task.await.unwrap(); + cx.run_until_parked(); + + multi_workspace_handle + .read_with(cx, |multi_workspace, cx| { + assert_eq!(multi_workspace.workspaces().len(), 2); + + let original_paths = + PathList::new(&multi_workspace.workspaces()[0].read(cx).root_paths(cx)); + assert_eq!(original_paths.paths(), &[PathBuf::from("/zed")]); + + let opened_paths = PathList::new(&opened_workspace.read(cx).root_paths(cx)); + assert_eq!( + opened_paths.paths(), + &[PathBuf::from("/foo"), PathBuf::from("/zed")] + ); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_open_workspace_for_paths_reuses_exact_directory_workspace_match( + cx: &mut TestAppContext, + ) { + init_test(cx); + cx.update(|cx| { + use feature_flags::FeatureFlagAppExt as _; + cx.update_flags(false, vec!["agent-v2".to_string()]); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/zed", json!({ "src": {} })).await; + fs.insert_tree("/foo", json!({ "lib": {} })).await; + + let project = Project::test(fs.clone(), ["/zed".as_ref(), "/foo".as_ref()], cx).await; + let multi_workspace_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + cx.run_until_parked(); + + let original_workspace = multi_workspace_handle + .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone()) + .unwrap(); + let original_workspace_id = original_workspace.entity_id(); + + let open_task = multi_workspace_handle + .update(cx, |multi_workspace, window, cx| { + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| { + workspace.open_workspace_for_paths( + true, + vec![PathBuf::from("/foo"), PathBuf::from("/zed")], + window, + cx, + ) + }) + }) + .unwrap(); + + let opened_workspace = open_task.await.unwrap(); + cx.run_until_parked(); + + multi_workspace_handle + .read_with(cx, |multi_workspace, _| { + assert_eq!(multi_workspace.workspaces().len(), 1); + assert_eq!(multi_workspace.active_workspace_index(), 0); + }) + .unwrap(); + + assert_eq!(opened_workspace.entity_id(), original_workspace_id); + } + #[gpui::test] async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) { init_test(cx);