Detailed changes
@@ -6131,30 +6131,48 @@ impl ProjectGroupKey {
Self { paths, host }
}
- pub fn display_name(&self) -> SharedString {
+ pub fn path_list(&self) -> &PathList {
+ &self.paths
+ }
+
+ pub fn display_name(
+ &self,
+ path_detail_map: &std::collections::HashMap<PathBuf, usize>,
+ ) -> SharedString {
let mut names = Vec::with_capacity(self.paths.paths().len());
for abs_path in self.paths.paths() {
- if let Some(name) = abs_path.file_name() {
- names.push(name.to_string_lossy().to_string());
+ let detail = path_detail_map.get(abs_path).copied().unwrap_or(0);
+ let suffix = path_suffix(abs_path, detail);
+ if !suffix.is_empty() {
+ names.push(suffix);
}
}
if names.is_empty() {
- // TODO: Can we do something better in this case?
"Empty Workspace".into()
} else {
names.join(", ").into()
}
}
- pub fn path_list(&self) -> &PathList {
- &self.paths
- }
-
pub fn host(&self) -> Option<RemoteConnectionOptions> {
self.host.clone()
}
}
+pub fn path_suffix(path: &Path, detail: usize) -> String {
+ let mut components: Vec<_> = path
+ .components()
+ .rev()
+ .filter_map(|component| match component {
+ std::path::Component::Normal(s) => Some(s.to_string_lossy()),
+ _ => None,
+ })
+ .take(detail + 1)
+ .collect();
+ components.reverse();
+ components.join("/")
+}
+
pub struct PathMatchCandidateSet {
pub snapshot: Snapshot,
pub include_ignored: bool,
@@ -99,27 +99,40 @@ pub async fn get_recent_projects(
.await
.unwrap_or_default();
- let entries: Vec<RecentProjectEntry> = workspaces
+ let filtered: Vec<_> = workspaces
.into_iter()
.filter(|(id, _, _, _)| Some(*id) != current_workspace_id)
.filter(|(_, location, _, _)| matches!(location, SerializedWorkspaceLocation::Local))
+ .collect();
+
+ let mut all_paths: Vec<PathBuf> = filtered
+ .iter()
+ .flat_map(|(_, _, path_list, _)| path_list.paths().iter().cloned())
+ .collect();
+ all_paths.sort();
+ all_paths.dedup();
+ let path_details =
+ util::disambiguate::compute_disambiguation_details(&all_paths, |path, detail| {
+ project::path_suffix(path, detail)
+ });
+ let path_detail_map: std::collections::HashMap<PathBuf, usize> =
+ all_paths.into_iter().zip(path_details).collect();
+
+ let entries: Vec<RecentProjectEntry> = filtered
+ .into_iter()
.map(|(workspace_id, _, path_list, timestamp)| {
let paths: Vec<PathBuf> = path_list.paths().to_vec();
let ordered_paths: Vec<&PathBuf> = path_list.ordered_paths().collect();
- let name = if ordered_paths.len() == 1 {
- ordered_paths[0]
- .file_name()
- .map(|n| n.to_string_lossy().to_string())
- .unwrap_or_else(|| ordered_paths[0].to_string_lossy().to_string())
- } else {
- ordered_paths
- .iter()
- .filter_map(|p| p.file_name())
- .map(|n| n.to_string_lossy().to_string())
- .collect::<Vec<_>>()
- .join(", ")
- };
+ let name = ordered_paths
+ .iter()
+ .map(|p| {
+ let detail = path_detail_map.get(*p).copied().unwrap_or(0);
+ project::path_suffix(p, detail)
+ })
+ .filter(|s| !s.is_empty())
+ .collect::<Vec<_>>()
+ .join(", ");
let full_path = ordered_paths
.iter()
@@ -172,6 +185,19 @@ fn get_open_folders(workspace: &Workspace, cx: &App) -> Vec<OpenFolderEntry> {
.map(|wt| wt.read(cx).id())
});
+ let mut all_paths: Vec<PathBuf> = visible_worktrees
+ .iter()
+ .map(|wt| wt.read(cx).abs_path().to_path_buf())
+ .collect();
+ all_paths.sort();
+ all_paths.dedup();
+ let path_details =
+ util::disambiguate::compute_disambiguation_details(&all_paths, |path, detail| {
+ project::path_suffix(path, detail)
+ });
+ let path_detail_map: std::collections::HashMap<PathBuf, usize> =
+ all_paths.into_iter().zip(path_details).collect();
+
let git_store = project.git_store().read(cx);
let repositories: Vec<_> = git_store.repositories().values().cloned().collect();
@@ -180,8 +206,9 @@ fn get_open_folders(workspace: &Workspace, cx: &App) -> Vec<OpenFolderEntry> {
.map(|worktree| {
let worktree_ref = worktree.read(cx);
let worktree_id = worktree_ref.id();
- let name = SharedString::from(worktree_ref.root_name().as_unix_str().to_string());
let path = worktree_ref.abs_path().to_path_buf();
+ let detail = path_detail_map.get(&path).copied().unwrap_or(0);
+ let name = SharedString::from(project::path_suffix(&path, detail));
let branch = get_branch_for_worktree(worktree_ref, &repositories, cx);
let is_active = active_worktree_id == Some(worktree_id);
OpenFolderEntry {
@@ -883,12 +883,27 @@ impl Sidebar {
(icon, icon_from_external_svg)
};
- for (group_key, group_workspaces) in mw.project_groups(cx) {
+ let groups: Vec<_> = mw.project_groups(cx).collect();
+
+ let mut all_paths: Vec<PathBuf> = groups
+ .iter()
+ .flat_map(|(key, _)| key.path_list().paths().iter().cloned())
+ .collect();
+ all_paths.sort();
+ all_paths.dedup();
+ let path_details =
+ util::disambiguate::compute_disambiguation_details(&all_paths, |path, detail| {
+ project::path_suffix(path, detail)
+ });
+ let path_detail_map: HashMap<PathBuf, usize> =
+ all_paths.into_iter().zip(path_details).collect();
+
+ for (group_key, group_workspaces) in &groups {
if group_key.path_list().paths().is_empty() {
continue;
}
- let label = group_key.display_name();
+ let label = group_key.display_name(&path_detail_map);
let is_collapsed = self.collapsed_groups.contains(&group_key);
let should_load_threads = !is_collapsed || !query.is_empty();
@@ -989,7 +1004,7 @@ impl Sidebar {
// Load any legacy threads for any single linked wortree of this project group.
let mut linked_worktree_paths = HashSet::new();
- for workspace in &group_workspaces {
+ for workspace in group_workspaces {
if workspace.read(cx).visible_worktrees(cx).count() != 1 {
continue;
}
@@ -1192,7 +1207,7 @@ impl Sidebar {
None
};
let thread_store = ThreadMetadataStore::global(cx);
- for ws in &group_workspaces {
+ for ws in group_workspaces {
if Some(ws.entity_id()) == draft_ws_id {
continue;
}
@@ -0,0 +1,202 @@
+use std::collections::HashMap;
+use std::hash::Hash;
+
+/// Computes the minimum detail level needed for each item so that no two items
+/// share the same description. Items whose descriptions are unique at level 0
+/// stay at 0; items that collide get their detail level incremented until either
+/// the collision is resolved or increasing the level no longer changes the
+/// description (preventing infinite loops for truly identical items).
+///
+/// The `get_description` closure must return a sequence that eventually reaches
+/// a "fixed point" where increasing `detail` no longer changes the output. If
+/// an item reaches its fixed point, it is assumed it will no longer change and
+/// will no longer be checked for collisions.
+pub fn compute_disambiguation_details<T, D>(
+ items: &[T],
+ get_description: impl Fn(&T, usize) -> D,
+) -> Vec<usize>
+where
+ D: Eq + Hash + Clone,
+{
+ let mut details = vec![0usize; items.len()];
+ let mut descriptions: HashMap<D, Vec<usize>> = HashMap::default();
+ let mut current_descriptions: Vec<D> =
+ items.iter().map(|item| get_description(item, 0)).collect();
+
+ loop {
+ let mut any_collisions = false;
+
+ for (index, (item, &detail)) in items.iter().zip(&details).enumerate() {
+ if detail > 0 {
+ let new_description = get_description(item, detail);
+ if new_description == current_descriptions[index] {
+ continue;
+ }
+ current_descriptions[index] = new_description;
+ }
+ descriptions
+ .entry(current_descriptions[index].clone())
+ .or_insert_with(Vec::new)
+ .push(index);
+ }
+
+ for (_, indices) in descriptions.drain() {
+ if indices.len() > 1 {
+ any_collisions = true;
+ for index in indices {
+ details[index] += 1;
+ }
+ }
+ }
+
+ if !any_collisions {
+ break;
+ }
+ }
+
+ details
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_no_conflicts() {
+ let items = vec!["alpha", "beta", "gamma"];
+ let details = compute_disambiguation_details(&items, |item, _detail| item.to_string());
+ assert_eq!(details, vec![0, 0, 0]);
+ }
+
+ #[test]
+ fn test_simple_two_way_conflict() {
+ // Two items with the same base name but different parents.
+ let items = vec![("src/foo.rs", "foo.rs"), ("lib/foo.rs", "foo.rs")];
+ let details = compute_disambiguation_details(&items, |item, detail| match detail {
+ 0 => item.1.to_string(),
+ _ => item.0.to_string(),
+ });
+ assert_eq!(details, vec![1, 1]);
+ }
+
+ #[test]
+ fn test_three_way_conflict() {
+ let items = vec![
+ ("foo.rs", "a/foo.rs"),
+ ("foo.rs", "b/foo.rs"),
+ ("foo.rs", "c/foo.rs"),
+ ];
+ let details = compute_disambiguation_details(&items, |item, detail| match detail {
+ 0 => item.0.to_string(),
+ _ => item.1.to_string(),
+ });
+ assert_eq!(details, vec![1, 1, 1]);
+ }
+
+ #[test]
+ fn test_deeper_conflict() {
+ // At detail 0, all three show "file.rs".
+ // At detail 1, items 0 and 1 both show "src/file.rs", item 2 shows "lib/file.rs".
+ // At detail 2, item 0 shows "a/src/file.rs", item 1 shows "b/src/file.rs".
+ let items = vec![
+ vec!["file.rs", "src/file.rs", "a/src/file.rs"],
+ vec!["file.rs", "src/file.rs", "b/src/file.rs"],
+ vec!["file.rs", "lib/file.rs", "x/lib/file.rs"],
+ ];
+ let details = compute_disambiguation_details(&items, |item, detail| {
+ let clamped = detail.min(item.len() - 1);
+ item[clamped].to_string()
+ });
+ assert_eq!(details, vec![2, 2, 1]);
+ }
+
+ #[test]
+ fn test_mixed_conflicting_and_unique() {
+ let items = vec![
+ ("src/foo.rs", "foo.rs"),
+ ("lib/foo.rs", "foo.rs"),
+ ("src/bar.rs", "bar.rs"),
+ ];
+ let details = compute_disambiguation_details(&items, |item, detail| match detail {
+ 0 => item.1.to_string(),
+ _ => item.0.to_string(),
+ });
+ assert_eq!(details, vec![1, 1, 0]);
+ }
+
+ #[test]
+ fn test_identical_items_terminates() {
+ // All items return the same description at every detail level.
+ // The algorithm must terminate rather than looping forever.
+ let items = vec!["same", "same", "same"];
+ let details = compute_disambiguation_details(&items, |item, _detail| item.to_string());
+ // After bumping to 1, the description doesn't change from level 0,
+ // so the items are skipped and the loop terminates.
+ assert_eq!(details, vec![1, 1, 1]);
+ }
+
+ #[test]
+ fn test_single_item() {
+ let items = vec!["only"];
+ let details = compute_disambiguation_details(&items, |item, _detail| item.to_string());
+ assert_eq!(details, vec![0]);
+ }
+
+ #[test]
+ fn test_empty_input() {
+ let items: Vec<&str> = vec![];
+ let details = compute_disambiguation_details(&items, |item, _detail| item.to_string());
+ let expected: Vec<usize> = vec![];
+ assert_eq!(details, expected);
+ }
+
+ #[test]
+ fn test_duplicate_paths_from_multiple_groups() {
+ use std::path::Path;
+
+ // Simulates the sidebar scenario: a path like /Users/rtfeldman/code/zed
+ // appears in two project groups (e.g. "zed" alone and "zed, roc").
+ // After deduplication, only unique paths should be disambiguated.
+ //
+ // Paths:
+ // /Users/rtfeldman/code/worktrees/zed/focal-arrow/zed (group 1)
+ // /Users/rtfeldman/code/zed (group 2)
+ // /Users/rtfeldman/code/zed (group 3, same path as group 2)
+ // /Users/rtfeldman/code/roc (group 3)
+ //
+ // A naive flat_map collects duplicates. The duplicate /code/zed entries
+ // collide with each other and drive the detail to the full path.
+ // The fix is to deduplicate before disambiguating.
+
+ fn path_suffix(path: &Path, detail: usize) -> String {
+ let mut components: Vec<_> = path
+ .components()
+ .rev()
+ .filter_map(|c| match c {
+ std::path::Component::Normal(s) => Some(s.to_string_lossy()),
+ _ => None,
+ })
+ .take(detail + 1)
+ .collect();
+ components.reverse();
+ components.join("/")
+ }
+
+ let all_paths: Vec<&Path> = vec![
+ Path::new("/Users/rtfeldman/code/worktrees/zed/focal-arrow/zed"),
+ Path::new("/Users/rtfeldman/code/zed"),
+ Path::new("/Users/rtfeldman/code/roc"),
+ ];
+
+ let details =
+ compute_disambiguation_details(&all_paths, |path, detail| path_suffix(path, detail));
+
+ // focal-arrow/zed and code/zed both end in "zed", so they need detail 1.
+ // "roc" is unique at detail 0.
+ assert_eq!(details, vec![1, 1, 0]);
+
+ assert_eq!(path_suffix(all_paths[0], details[0]), "focal-arrow/zed");
+ assert_eq!(path_suffix(all_paths[1], details[1]), "code/zed");
+ assert_eq!(path_suffix(all_paths[2], details[2]), "roc");
+ }
+}
@@ -1,5 +1,6 @@
pub mod archive;
pub mod command;
+pub mod disambiguate;
pub mod fs;
pub mod markdown;
pub mod path_list;
@@ -4897,36 +4897,9 @@ fn dirty_message_for(buffer_path: Option<ProjectPath>, path_style: PathStyle) ->
}
pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
- let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
- let mut tab_descriptions = HashMap::default();
- let mut done = false;
- while !done {
- done = true;
-
- // Store item indices by their tab description.
- for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() {
- let description = item.tab_content_text(*detail, cx);
- if *detail == 0 || description != item.tab_content_text(detail - 1, cx) {
- tab_descriptions
- .entry(description)
- .or_insert(Vec::new())
- .push(ix);
- }
- }
-
- // If two or more items have the same tab description, increase their level
- // of detail and try again.
- for (_, item_ixs) in tab_descriptions.drain() {
- if item_ixs.len() > 1 {
- done = false;
- for ix in item_ixs {
- tab_details[ix] += 1;
- }
- }
- }
- }
-
- tab_details
+ util::disambiguate::compute_disambiguation_details(items, |item, detail| {
+ item.tab_content_text(detail, cx)
+ })
}
pub fn render_item_indicator(item: Box<dyn ItemHandle>, cx: &App) -> Option<Indicator> {
@@ -573,6 +573,27 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()>
}
}
+ // Run Test: Sidebar with duplicate project names
+ println!("\n--- Test: sidebar_duplicate_names ---");
+ match run_sidebar_duplicate_project_names_visual_tests(
+ app_state.clone(),
+ &mut cx,
+ update_baseline,
+ ) {
+ Ok(TestResult::Passed) => {
+ println!("✓ sidebar_duplicate_names: PASSED");
+ passed += 1;
+ }
+ Ok(TestResult::BaselineUpdated(_)) => {
+ println!("✓ sidebar_duplicate_names: Baselines updated");
+ updated += 1;
+ }
+ Err(e) => {
+ eprintln!("✗ sidebar_duplicate_names: FAILED - {}", e);
+ failed += 1;
+ }
+ }
+
// Run Test 9: Tool Permissions Settings UI visual test
println!("\n--- Test 9: tool_permissions_settings ---");
match run_tool_permissions_visual_tests(app_state.clone(), &mut cx, update_baseline) {
@@ -3069,6 +3090,279 @@ fn run_git_command(args: &[&str], dir: &std::path::Path) -> Result<()> {
Ok(())
}
+#[cfg(target_os = "macos")]
+/// Helper to create a project, add a worktree at the given path, and return the project.
+fn create_project_with_worktree(
+ worktree_dir: &Path,
+ app_state: &Arc<AppState>,
+ cx: &mut VisualTestAppContext,
+) -> Result<Entity<Project>> {
+ let project = cx.update(|cx| {
+ project::Project::local(
+ app_state.client.clone(),
+ app_state.node_runtime.clone(),
+ app_state.user_store.clone(),
+ app_state.languages.clone(),
+ app_state.fs.clone(),
+ None,
+ project::LocalProjectFlags {
+ init_worktree_trust: false,
+ ..Default::default()
+ },
+ cx,
+ )
+ });
+
+ let add_task = cx.update(|cx| {
+ project.update(cx, |project, cx| {
+ project.find_or_create_worktree(worktree_dir, true, cx)
+ })
+ });
+
+ cx.background_executor.allow_parking();
+ cx.foreground_executor
+ .block_test(add_task)
+ .context("Failed to add worktree")?;
+ cx.background_executor.forbid_parking();
+
+ cx.run_until_parked();
+ Ok(project)
+}
+
+#[cfg(target_os = "macos")]
+fn open_sidebar_test_window(
+ projects: Vec<Entity<Project>>,
+ app_state: &Arc<AppState>,
+ cx: &mut VisualTestAppContext,
+) -> Result<WindowHandle<MultiWorkspace>> {
+ anyhow::ensure!(!projects.is_empty(), "need at least one project");
+
+ let window_size = size(px(400.0), px(600.0));
+ let bounds = Bounds {
+ origin: point(px(0.0), px(0.0)),
+ size: window_size,
+ };
+
+ let mut projects_iter = projects.into_iter();
+ let first_project = projects_iter
+ .next()
+ .ok_or_else(|| anyhow::anyhow!("need at least one project"))?;
+ let remaining: Vec<_> = projects_iter.collect();
+
+ let multi_workspace_window: WindowHandle<MultiWorkspace> = cx
+ .update(|cx| {
+ cx.open_window(
+ WindowOptions {
+ window_bounds: Some(WindowBounds::Windowed(bounds)),
+ focus: false,
+ show: false,
+ ..Default::default()
+ },
+ |window, cx| {
+ let first_ws = cx.new(|cx| {
+ Workspace::new(None, first_project.clone(), app_state.clone(), window, cx)
+ });
+ cx.new(|cx| {
+ let mut mw = MultiWorkspace::new(first_ws, window, cx);
+ for project in remaining {
+ let ws = cx.new(|cx| {
+ Workspace::new(None, project, app_state.clone(), window, cx)
+ });
+ mw.activate(ws, window, cx);
+ }
+ mw
+ })
+ },
+ )
+ })
+ .context("Failed to open MultiWorkspace window")?;
+
+ cx.run_until_parked();
+
+ // Create the sidebar outside the MultiWorkspace update to avoid a
+ // re-entrant read panic (Sidebar::new reads the MultiWorkspace).
+ let sidebar = cx
+ .update_window(multi_workspace_window.into(), |root_view, window, cx| {
+ let mw_handle: Entity<MultiWorkspace> = root_view
+ .downcast()
+ .map_err(|_| anyhow::anyhow!("Failed to downcast root view to MultiWorkspace"))?;
+ Ok::<_, anyhow::Error>(cx.new(|cx| sidebar::Sidebar::new(mw_handle, window, cx)))
+ })
+ .context("Failed to create sidebar")??;
+
+ multi_workspace_window
+ .update(cx, |mw, _window, cx| {
+ mw.register_sidebar(sidebar.clone(), cx);
+ })
+ .context("Failed to register sidebar")?;
+
+ cx.run_until_parked();
+
+ // Open the sidebar
+ multi_workspace_window
+ .update(cx, |mw, window, cx| {
+ mw.toggle_sidebar(window, cx);
+ })
+ .context("Failed to toggle sidebar")?;
+
+ // Let rendering settle
+ for _ in 0..10 {
+ cx.advance_clock(Duration::from_millis(100));
+ cx.run_until_parked();
+ }
+
+ // Refresh the window
+ cx.update_window(multi_workspace_window.into(), |_, window, _cx| {
+ window.refresh();
+ })?;
+
+ cx.run_until_parked();
+
+ Ok(multi_workspace_window)
+}
+
+#[cfg(target_os = "macos")]
+fn cleanup_sidebar_test_window(
+ window: WindowHandle<MultiWorkspace>,
+ cx: &mut VisualTestAppContext,
+) -> Result<()> {
+ window.update(cx, |mw, _window, cx| {
+ for workspace in mw.workspaces() {
+ let project = workspace.read(cx).project().clone();
+ project.update(cx, |project, cx| {
+ let ids: Vec<_> = project.worktrees(cx).map(|wt| wt.read(cx).id()).collect();
+ for id in ids {
+ project.remove_worktree(id, cx);
+ }
+ });
+ }
+ })?;
+
+ cx.run_until_parked();
+
+ cx.update_window(window.into(), |_, window, _cx| {
+ window.remove_window();
+ })?;
+
+ cx.run_until_parked();
+
+ for _ in 0..15 {
+ cx.advance_clock(Duration::from_millis(100));
+ cx.run_until_parked();
+ }
+
+ Ok(())
+}
+
+#[cfg(target_os = "macos")]
+fn run_sidebar_duplicate_project_names_visual_tests(
+ app_state: Arc<AppState>,
+ cx: &mut VisualTestAppContext,
+ update_baseline: bool,
+) -> Result<TestResult> {
+ let temp_dir = tempfile::tempdir()?;
+ let temp_path = temp_dir.keep();
+ let canonical_temp = temp_path.canonicalize()?;
+
+ // Create directory structure where every leaf directory is named "zed" but
+ // lives at a distinct path. This lets us test that the sidebar correctly
+ // disambiguates projects whose names would otherwise collide.
+ //
+ // code/zed/ — project1 (single worktree)
+ // code/foo/zed/ — project2 (single worktree)
+ // code/bar/zed/ — project3, first worktree
+ // code/baz/zed/ — project3, second worktree
+ //
+ // No two projects share a worktree path, so ProjectGroupBuilder will
+ // place each in its own group.
+ let code_zed = canonical_temp.join("code").join("zed");
+ let foo_zed = canonical_temp.join("code").join("foo").join("zed");
+ let bar_zed = canonical_temp.join("code").join("bar").join("zed");
+ let baz_zed = canonical_temp.join("code").join("baz").join("zed");
+ std::fs::create_dir_all(&code_zed)?;
+ std::fs::create_dir_all(&foo_zed)?;
+ std::fs::create_dir_all(&bar_zed)?;
+ std::fs::create_dir_all(&baz_zed)?;
+
+ cx.update(|cx| {
+ cx.update_flags(true, vec!["agent-v2".to_string()]);
+ });
+
+ let mut has_baseline_update = None;
+
+ // Two single-worktree projects whose leaf name is "zed"
+ {
+ let project1 = create_project_with_worktree(&code_zed, &app_state, cx)?;
+ let project2 = create_project_with_worktree(&foo_zed, &app_state, cx)?;
+
+ let window = open_sidebar_test_window(vec![project1, project2], &app_state, cx)?;
+
+ let result = run_visual_test(
+ "sidebar_two_projects_same_leaf_name",
+ window.into(),
+ cx,
+ update_baseline,
+ );
+
+ cleanup_sidebar_test_window(window, cx)?;
+ match result? {
+ TestResult::Passed => {}
+ TestResult::BaselineUpdated(path) => {
+ has_baseline_update = Some(path);
+ }
+ }
+ }
+
+ // Three projects, third has two worktrees (all leaf names "zed")
+ //
+ // project1: code/zed
+ // project2: code/foo/zed
+ // project3: code/bar/zed + code/baz/zed
+ //
+ // Each project has a unique set of worktree paths, so they form
+ // separate groups. The sidebar must disambiguate all three.
+ {
+ let project1 = create_project_with_worktree(&code_zed, &app_state, cx)?;
+ let project2 = create_project_with_worktree(&foo_zed, &app_state, cx)?;
+
+ let project3 = create_project_with_worktree(&bar_zed, &app_state, cx)?;
+ let add_second_worktree = cx.update(|cx| {
+ project3.update(cx, |project, cx| {
+ project.find_or_create_worktree(&baz_zed, true, cx)
+ })
+ });
+ cx.background_executor.allow_parking();
+ cx.foreground_executor
+ .block_test(add_second_worktree)
+ .context("Failed to add second worktree to project 3")?;
+ cx.background_executor.forbid_parking();
+ cx.run_until_parked();
+
+ let window = open_sidebar_test_window(vec![project1, project2, project3], &app_state, cx)?;
+
+ let result = run_visual_test(
+ "sidebar_three_projects_with_multi_worktree",
+ window.into(),
+ cx,
+ update_baseline,
+ );
+
+ cleanup_sidebar_test_window(window, cx)?;
+ match result? {
+ TestResult::Passed => {}
+ TestResult::BaselineUpdated(path) => {
+ has_baseline_update = Some(path);
+ }
+ }
+ }
+
+ if let Some(path) = has_baseline_update {
+ Ok(TestResult::BaselineUpdated(path))
+ } else {
+ Ok(TestResult::Passed)
+ }
+}
+
#[cfg(all(target_os = "macos", feature = "visual-tests"))]
fn run_start_thread_in_selector_visual_tests(
app_state: Arc<AppState>,