Detailed changes
@@ -1130,6 +1130,7 @@ impl PickerDelegate for RecentProjectsDelegate {
return;
};
+ let key = key.clone();
let path_list = key.path_list().clone();
if let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() {
cx.defer(move |cx| {
@@ -1137,6 +1138,7 @@ impl PickerDelegate for RecentProjectsDelegate {
.update(cx, |multi_workspace, window, cx| {
multi_workspace.find_or_create_local_workspace(
path_list,
+ Some(key.clone()),
&[],
window,
cx,
@@ -2995,15 +2995,17 @@ impl Sidebar {
ListEntry::Thread(t)
if !t.is_draft && t.metadata.session_id.as_ref() != Some(session_id) =>
{
- let workspace_paths = match &t.workspace {
- ThreadEntryWorkspace::Open(ws) => {
- PathList::new(&ws.read(cx).root_paths(cx))
- }
- ThreadEntryWorkspace::Closed { folder_paths, .. } => {
- folder_paths.clone()
- }
+ let (workspace_paths, project_group_key) = match &t.workspace {
+ ThreadEntryWorkspace::Open(ws) => (
+ PathList::new(&ws.read(cx).root_paths(cx)),
+ ws.read(cx).project_group_key(cx),
+ ),
+ ThreadEntryWorkspace::Closed {
+ folder_paths,
+ project_group_key,
+ } => (folder_paths.clone(), project_group_key.clone()),
};
- Some((t.metadata.clone(), workspace_paths))
+ Some((t.metadata.clone(), workspace_paths, project_group_key))
}
_ => None,
})
@@ -3119,13 +3121,16 @@ impl Sidebar {
let multi_workspace = self.multi_workspace.upgrade().unwrap();
let session_id = session_id.clone();
- let fallback_paths = neighbor
+ let (fallback_paths, project_group_key) = neighbor
.as_ref()
- .map(|(_, paths)| paths.clone())
+ .map(|(_, paths, project_group_key)| (paths.clone(), project_group_key.clone()))
.unwrap_or_else(|| {
workspaces_to_remove
.first()
- .map(|ws| ws.read(cx).project_group_key(cx).path_list().clone())
+ .map(|ws| {
+ let key = ws.read(cx).project_group_key(cx);
+ (key.path_list().clone(), key)
+ })
.unwrap_or_default()
});
@@ -3134,14 +3139,20 @@ impl Sidebar {
mw.remove(
workspaces_to_remove,
move |this, window, cx| {
- this.find_or_create_local_workspace(fallback_paths, &excluded, window, cx)
+ this.find_or_create_local_workspace(
+ fallback_paths,
+ Some(project_group_key),
+ &excluded,
+ window,
+ cx,
+ )
},
window,
cx,
)
});
- let neighbor_metadata = neighbor.map(|(metadata, _)| metadata);
+ let neighbor_metadata = neighbor.map(|(metadata, _, _)| metadata);
let thread_folder_paths = thread_folder_paths.clone();
cx.spawn_in(window, async move |this, cx| {
if !remove_task.await? {
@@ -3172,7 +3183,7 @@ impl Sidebar {
.detach_and_log_err(cx);
} else if !close_item_tasks.is_empty() {
let session_id = session_id.clone();
- let neighbor_metadata = neighbor.map(|(metadata, _)| metadata);
+ let neighbor_metadata = neighbor.map(|(metadata, _, _)| metadata);
let thread_folder_paths = thread_folder_paths.clone();
cx.spawn_in(window, async move |this, cx| {
for task in close_item_tasks {
@@ -3198,7 +3209,7 @@ impl Sidebar {
})
.detach_and_log_err(cx);
} else {
- let neighbor_metadata = neighbor.map(|(metadata, _)| metadata);
+ let neighbor_metadata = neighbor.map(|(metadata, _, _)| metadata);
let in_flight = thread_id
.and_then(|tid| self.start_archive_worktree_task(tid, roots_to_archive, cx));
self.archive_and_activate(
@@ -1,4 +1,6 @@
use anyhow::Result;
+use fs::Fs;
+
use gpui::PathPromptOptions;
use gpui::{
AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
@@ -996,6 +998,7 @@ impl MultiWorkspace {
if let Some(neighbor_key) = neighbor_key {
return this.find_or_create_local_workspace(
neighbor_key.path_list().clone(),
+ Some(neighbor_key.clone()),
&excluded_workspaces,
window,
cx,
@@ -1133,7 +1136,13 @@ impl MultiWorkspace {
}
let Some(connection_options) = host else {
- return self.find_or_create_local_workspace(paths, &[], window, cx);
+ return self.find_or_create_local_workspace(
+ paths,
+ provisional_project_group_key,
+ &[],
+ window,
+ cx,
+ );
};
let app_state = self.workspace().read(cx).app_state().clone();
@@ -1191,6 +1200,7 @@ impl MultiWorkspace {
pub fn find_or_create_local_workspace(
&mut self,
path_list: PathList,
+ project_group: Option<ProjectGroupKey>,
excluding: &[Entity<Workspace>],
window: &mut Window,
cx: &mut Context<Self>,
@@ -1204,12 +1214,57 @@ impl MultiWorkspace {
let paths = path_list.paths().to_vec();
let app_state = self.workspace().read(cx).app_state().clone();
let requesting_window = window.window_handle().downcast::<MultiWorkspace>();
+ let fs = <dyn Fs>::global(cx);
+ let excluding = excluding.to_vec();
cx.spawn(async move |_this, cx| {
+ let effective_path_list = if let Some(project_group) = project_group {
+ let metadata_tasks: Vec<_> = paths
+ .iter()
+ .map(|path| fs.metadata(path.as_path()))
+ .collect();
+ let metadata_results = futures::future::join_all(metadata_tasks).await;
+ // Only fall back when every path is definitely absent; real
+ // filesystem errors should not be treated as "missing".
+ let all_paths_missing = !paths.is_empty()
+ && metadata_results
+ .into_iter()
+ // Ok(None) means the path is definitely absent
+ .all(|result| matches!(result, Ok(None)));
+
+ if all_paths_missing {
+ project_group.path_list().clone()
+ } else {
+ PathList::new(&paths)
+ }
+ } else {
+ PathList::new(&paths)
+ };
+
+ if let Some(requesting_window) = requesting_window
+ && let Some(workspace) = requesting_window
+ .update(cx, |multi_workspace, window, cx| {
+ multi_workspace
+ .workspace_for_paths_excluding(
+ &effective_path_list,
+ None,
+ &excluding,
+ cx,
+ )
+ .inspect(|workspace| {
+ multi_workspace.activate(workspace.clone(), window, cx);
+ })
+ })
+ .ok()
+ .flatten()
+ {
+ return Ok(workspace);
+ }
+
let result = cx
.update(|cx| {
Workspace::new_local(
- paths,
+ effective_path_list.paths().to_vec(),
app_state,
requesting_window,
None,
@@ -1755,7 +1810,7 @@ impl MultiWorkspace {
cx: &mut Context<Self>,
) -> Task<Result<Entity<Workspace>>> {
if self.multi_workspace_enabled(cx) {
- self.find_or_create_local_workspace(PathList::new(&paths), &[], window, cx)
+ self.find_or_create_local_workspace(PathList::new(&paths), None, &[], window, cx)
} else {
let workspace = self.workspace().clone();
cx.spawn_in(window, async move |_this, cx| {
@@ -2,7 +2,7 @@ use std::path::PathBuf;
use super::*;
use client::proto;
-use fs::FakeFs;
+use fs::{FakeFs, Fs};
use gpui::TestAppContext;
use project::DisableAiSettings;
use serde_json::json;
@@ -433,6 +433,7 @@ async fn test_find_or_create_local_workspace_reuses_active_workspace_when_sideba
.update_in(cx, |mw, window, cx| {
mw.find_or_create_local_workspace(
PathList::new(&[PathBuf::from("/root_a")]),
+ None,
&[],
window,
cx,
@@ -461,6 +462,73 @@ async fn test_find_or_create_local_workspace_reuses_active_workspace_when_sideba
});
}
+#[gpui::test]
+async fn test_find_or_create_workspace_uses_project_group_key_when_paths_are_missing(
+ cx: &mut TestAppContext,
+) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/project",
+ json!({
+ ".git": {},
+ "src": {},
+ }),
+ )
+ .await;
+ cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
+ let project = Project::test(fs.clone(), ["/project".as_ref()], cx).await;
+ project
+ .update(cx, |project, cx| project.git_scans_complete(cx))
+ .await;
+
+ let project_group_key = project.read_with(cx, |project, cx| project.project_group_key(cx));
+
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
+
+ let main_workspace = multi_workspace.read_with(cx, |mw, _cx| mw.workspace().clone());
+ let main_workspace_id = main_workspace.entity_id();
+
+ let workspace = multi_workspace
+ .update_in(cx, |mw, window, cx| {
+ mw.find_or_create_workspace(
+ PathList::new(&[PathBuf::from("/wt-feature-a")]),
+ None,
+ Some(project_group_key.clone()),
+ |_options, _window, _cx| Task::ready(Ok(None)),
+ window,
+ cx,
+ )
+ })
+ .await
+ .expect("opening a missing linked-worktree path should fall back to the project group key workspace");
+
+ assert_eq!(
+ workspace.entity_id(),
+ main_workspace_id,
+ "missing linked-worktree paths should reuse the main worktree workspace from the project group key"
+ );
+
+ multi_workspace.read_with(cx, |mw, cx| {
+ assert_eq!(
+ mw.workspace().entity_id(),
+ main_workspace_id,
+ "the active workspace should remain the main worktree workspace"
+ );
+ assert_eq!(
+ PathList::new(&mw.workspace().read(cx).root_paths(cx)),
+ project_group_key.path_list().clone(),
+ "the activated workspace should use the project group key path list rather than the missing linked-worktree path"
+ );
+ assert_eq!(
+ mw.workspaces().count(),
+ 1,
+ "falling back to the project group key should not create a second workspace"
+ );
+ });
+}
+
#[gpui::test]
async fn test_find_or_create_local_workspace_reuses_active_workspace_after_sidebar_open(
cx: &mut TestAppContext,
@@ -492,6 +560,7 @@ async fn test_find_or_create_local_workspace_reuses_active_workspace_after_sideb
.update_in(cx, |mw, window, cx| {
mw.find_or_create_local_workspace(
PathList::new(&[PathBuf::from("/root_a")]),
+ None,
&[],
window,
cx,
@@ -5048,7 +5048,7 @@ mod tests {
mw.remove(
vec![workspace_a.clone()],
move |this, window, cx| {
- this.find_or_create_local_workspace(path_list, &excluded, window, cx)
+ this.find_or_create_local_workspace(path_list, None, &excluded, window, cx)
},
window,
cx,
@@ -9875,11 +9875,13 @@ async fn open_remote_project_inner(
for path in paths {
let result = cx
- .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))
+ .update(|cx| {
+ Workspace::project_path_for_path(project.clone(), path.as_path(), true, cx)
+ })
.await;
match result {
Ok((_, project_path)) => {
- project_paths_to_open.push((path.clone(), Some(project_path)));
+ project_paths_to_open.push((path, Some(project_path)));
}
Err(error) => {
project_path_errors.push(error);