diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 019f4975c41174337593a31b892ee6bdde0dc151..53eebc4d2604e91caa4fc0fec57f27345becbae0 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -670,6 +670,19 @@ impl Sidebar { let mut absorbed: HashMap = HashMap::new(); let mut pending: HashMap, Vec<(usize, SharedString, Arc)>> = HashMap::new(); let mut absorbed_workspace_by_path: HashMap, usize> = HashMap::new(); + let workspace_indices_by_path: HashMap, Vec> = workspaces + .iter() + .enumerate() + .flat_map(|(index, workspace)| { + let paths = workspace_path_list(workspace, cx).paths().to_vec(); + paths + .into_iter() + .map(move |path| (Arc::from(path.as_path()), index)) + }) + .fold(HashMap::new(), |mut map, (path, index)| { + map.entry(path).or_default().push(index); + map + }); for (i, workspace) in workspaces.iter().enumerate() { for snapshot in root_repository_snapshots(workspace, cx) { @@ -677,6 +690,29 @@ impl Sidebar { main_repo_workspace .entry(snapshot.work_directory_abs_path.clone()) .or_insert(i); + + for git_worktree in snapshot.linked_worktrees() { + let worktree_path: Arc = Arc::from(git_worktree.path.as_path()); + if let Some(worktree_indices) = + workspace_indices_by_path.get(worktree_path.as_ref()) + { + for &worktree_idx in worktree_indices { + if worktree_idx == i { + continue; + } + + let worktree_name = linked_worktree_short_name( + &snapshot.original_repo_abs_path, + &git_worktree.path, + ) + .unwrap_or_default(); + absorbed.insert(worktree_idx, (i, worktree_name.clone())); + absorbed_workspace_by_path + .insert(worktree_path.clone(), worktree_idx); + } + } + } + if let Some(waiting) = pending.remove(&snapshot.work_directory_abs_path) { for (ws_idx, name, ws_path) in waiting { absorbed.insert(ws_idx, (i, name)); @@ -2120,6 +2156,7 @@ impl Sidebar { cx.spawn_in(window, async move |this, cx| { let workspace = open_task.await?; + this.update_in(cx, |this, window, cx| { this.activate_thread(agent, session_info, &workspace, window, cx); })?; @@ -6128,6 +6165,167 @@ mod tests { ); } + #[gpui::test] + async fn test_clicking_worktree_thread_does_not_briefly_render_as_separate_project( + cx: &mut TestAppContext, + ) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + fs.insert_tree( + "/project", + serde_json::json!({ + ".git": { + "worktrees": { + "feature-a": { + "commondir": "../../", + "HEAD": "ref: refs/heads/feature-a", + }, + }, + }, + "src": {}, + }), + ) + .await; + + fs.insert_tree( + "/wt-feature-a", + serde_json::json!({ + ".git": "gitdir: /project/.git/worktrees/feature-a", + "src": {}, + }), + ) + .await; + + fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| { + state.worktrees.push(git::repository::Worktree { + path: std::path::PathBuf::from("/wt-feature-a"), + ref_name: Some("refs/heads/feature-a".into()), + sha: "aaa".into(), + }); + }) + .unwrap(); + + cx.update(|cx| ::set_global(fs.clone(), cx)); + + let main_project = project::Project::test(fs.clone(), ["/project".as_ref()], cx).await; + main_project + .update(cx, |p, cx| p.git_scans_complete(cx)) + .await; + + let (multi_workspace, cx) = cx.add_window_view(|window, cx| { + MultiWorkspace::test_new(main_project.clone(), window, cx) + }); + let sidebar = setup_sidebar(&multi_workspace, cx); + + let paths_wt = PathList::new(&[std::path::PathBuf::from("/wt-feature-a")]); + save_named_thread_metadata("thread-wt", "WT Thread", &paths_wt, cx).await; + + multi_workspace.update_in(cx, |_, _window, cx| cx.notify()); + cx.run_until_parked(); + + assert_eq!( + visible_entries_as_strings(&sidebar, cx), + vec!["v [project]", " WT Thread {wt-feature-a}"], + ); + + open_and_focus_sidebar(&sidebar, cx); + sidebar.update_in(cx, |sidebar, _window, _cx| { + sidebar.selection = Some(1); + }); + + let assert_sidebar_state = |sidebar: &mut Sidebar, _cx: &mut Context| { + let mut project_headers = sidebar.contents.entries.iter().filter_map(|entry| { + if let ListEntry::ProjectHeader { label, .. } = entry { + Some(label.as_ref()) + } else { + None + } + }); + + let Some(project_header) = project_headers.next() else { + panic!("expected exactly one sidebar project header named `project`, found none"); + }; + assert_eq!( + project_header, "project", + "expected the only sidebar project header to be `project`" + ); + if let Some(unexpected_header) = project_headers.next() { + panic!( + "expected exactly one sidebar project header named `project`, found extra header `{unexpected_header}`" + ); + } + + let mut saw_expected_thread = false; + for entry in &sidebar.contents.entries { + match entry { + ListEntry::ProjectHeader { label, .. } => { + assert_eq!( + label.as_ref(), + "project", + "expected the only sidebar project header to be `project`" + ); + } + ListEntry::Thread(thread) + if thread + .session_info + .title + .as_ref() + .map(|title| title.as_ref()) + == Some("WT Thread") + && thread.worktree_name.as_ref().map(|name| name.as_ref()) + == Some("wt-feature-a") => + { + saw_expected_thread = true; + } + ListEntry::Thread(thread) => { + let title = thread + .session_info + .title + .as_ref() + .map(|title| title.as_ref()) + .unwrap_or("Untitled"); + let worktree_name = thread + .worktree_name + .as_ref() + .map(|name| name.as_ref()) + .unwrap_or(""); + panic!( + "unexpected sidebar thread while opening linked worktree thread: title=`{title}`, worktree=`{worktree_name}`" + ); + } + ListEntry::ViewMore { .. } => { + panic!("unexpected `View More` entry while opening linked worktree thread"); + } + ListEntry::NewThread { .. } => { + panic!( + "unexpected `New Thread` entry while opening linked worktree thread" + ); + } + } + } + + assert!( + saw_expected_thread, + "expected the sidebar to keep showing `WT Thread {{wt-feature-a}}` under `project`" + ); + }; + + sidebar + .update(cx, |_, cx| cx.observe_self(assert_sidebar_state)) + .detach(); + + let window = cx.windows()[0]; + cx.update_window(window, |_, window, cx| { + window.dispatch_action(Confirm.boxed_clone(), cx); + }) + .unwrap(); + + cx.run_until_parked(); + + sidebar.update(cx, assert_sidebar_state); + } + #[gpui::test] async fn test_clicking_absorbed_worktree_thread_activates_worktree_workspace( cx: &mut TestAppContext,