Detailed changes
@@ -15,39 +15,7 @@ use std::{
};
use gpui::{App, Entity};
-use ui::SharedString;
-use workspace::{MultiWorkspace, PathList, Workspace};
-
-/// Identifies a project group by a set of paths the workspaces in this group
-/// have.
-///
-/// Paths are mapped to their main worktree path first so we can group
-/// workspaces by main repos.
-#[derive(PartialEq, Eq, Hash, Clone)]
-pub struct ProjectGroupName {
- path_list: PathList,
-}
-
-impl ProjectGroupName {
- pub fn display_name(&self) -> SharedString {
- let mut names = Vec::with_capacity(self.path_list.paths().len());
- for abs_path in self.path_list.paths() {
- if let Some(name) = abs_path.file_name() {
- names.push(name.to_string_lossy().to_string());
- }
- }
- 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.path_list
- }
-}
+use workspace::{MultiWorkspace, PathList, ProjectGroupKey, Workspace};
#[derive(Default)]
pub struct ProjectGroup {
@@ -88,7 +56,7 @@ impl ProjectGroup {
pub struct ProjectGroupBuilder {
/// Maps git repositories' work_directory_abs_path to their original_repo_abs_path
directory_mappings: HashMap<PathBuf, PathBuf>,
- project_groups: VecMap<ProjectGroupName, ProjectGroup>,
+ project_groups: VecMap<ProjectGroupKey, ProjectGroup>,
}
impl ProjectGroupBuilder {
@@ -111,16 +79,16 @@ impl ProjectGroupBuilder {
// Second pass: group each workspace using canonical paths derived
// from the full set of mappings.
for workspace in mw.workspaces() {
- let group_name = builder.canonical_workspace_paths(workspace, cx);
+ let group_key = workspace.read(cx).project_group_key(cx);
builder
- .project_group_entry(&group_name)
+ .project_group_entry(&group_key)
.add_workspace(workspace, cx);
}
builder
}
- fn project_group_entry(&mut self, name: &ProjectGroupName) -> &mut ProjectGroup {
- self.project_groups.entry_ref(name).or_insert_default()
+ fn project_group_entry(&mut self, key: &ProjectGroupKey) -> &mut ProjectGroup {
+ self.project_groups.entry_ref(key).or_insert_default()
}
fn add_mapping(&mut self, work_directory: &Path, original_repo: &Path) {
@@ -150,23 +118,6 @@ impl ProjectGroupBuilder {
}
}
- /// Derives the canonical group name for a workspace by canonicalizing
- /// each of its root paths using the builder's directory mappings.
- fn canonical_workspace_paths(
- &self,
- workspace: &Entity<Workspace>,
- cx: &App,
- ) -> ProjectGroupName {
- let root_paths = workspace.read(cx).root_paths(cx);
- let paths: Vec<_> = root_paths
- .iter()
- .map(|p| self.canonicalize_path(p).to_path_buf())
- .collect();
- ProjectGroupName {
- path_list: PathList::new(&paths),
- }
- }
-
pub fn canonicalize_path<'a>(&'a self, path: &'a Path) -> &'a Path {
self.directory_mappings
.get(path)
@@ -203,9 +154,13 @@ impl ProjectGroupBuilder {
PathList::new(&paths)
}
- pub fn groups(&self) -> impl Iterator<Item = (&ProjectGroupName, &ProjectGroup)> {
+ pub fn groups(&self) -> impl Iterator<Item = (&ProjectGroupKey, &ProjectGroup)> {
self.project_groups.iter()
}
+
+ pub fn ensure_group(&mut self, key: &ProjectGroupKey) {
+ self.project_groups.entry_ref(key).or_insert_default();
+ }
}
#[cfg(test)]
@@ -224,7 +224,6 @@ enum ListEntry {
},
NewThread {
path_list: PathList,
- workspace: Entity<Workspace>,
worktrees: Vec<WorktreeInfo>,
},
}
@@ -239,7 +238,7 @@ impl ListEntry {
ThreadEntryWorkspace::Closed(_) => None,
},
ListEntry::ViewMore { .. } => None,
- ListEntry::NewThread { workspace, .. } => Some(workspace.clone()),
+ ListEntry::NewThread { .. } => None,
}
}
@@ -767,7 +766,7 @@ impl Sidebar {
// Use ProjectGroupBuilder to canonically group workspaces by their
// main git repository. This replaces the manual absorbed-workspace
// detection that was here before.
- let project_groups = ProjectGroupBuilder::from_multiworkspace(mw, cx);
+ let mut project_groups = ProjectGroupBuilder::from_multiworkspace(mw, cx);
let has_open_projects = workspaces
.iter()
@@ -785,13 +784,18 @@ impl Sidebar {
(icon, icon_from_external_svg)
};
- for (group_name, group) in project_groups.groups() {
- let path_list = group_name.path_list().clone();
+ // Make sure all the groups the MultiWorkspace cares about are also present
+ for mw_groups in mw.project_group_keys(cx) {
+ project_groups.ensure_group(&mw_groups);
+ }
+
+ for (group_key, group) in project_groups.groups() {
+ let path_list = group_key.main_worktree_paths.clone();
if path_list.paths().is_empty() {
continue;
}
- let label = group_name.display_name();
+ let label = group_key.display_name();
let is_collapsed = self.collapsed_groups.contains(&path_list);
let should_load_threads = !is_collapsed || !query.is_empty();
@@ -807,6 +811,7 @@ impl Sidebar {
.as_ref()
.filter(|_| is_active)
.unwrap_or_else(|| group.main_workspace(cx));
+ // TODO: if we don't have a main workspace, create one for the main worktrees.
// Collect live thread infos from all workspaces in this group.
let live_infos: Vec<_> = group
@@ -1044,7 +1049,6 @@ impl Sidebar {
for (workspace, worktrees) in &threadless_workspaces {
entries.push(ListEntry::NewThread {
path_list: path_list.clone(),
- workspace: workspace.clone(),
worktrees: worktrees.clone(),
});
}
@@ -1057,7 +1061,6 @@ impl Sidebar {
let worktrees = worktree_info_from_thread_paths(&ws_path_list, &project_groups);
entries.push(ListEntry::NewThread {
path_list: path_list.clone(),
- workspace: representative_workspace.clone(),
worktrees,
});
}
@@ -1218,17 +1221,8 @@ impl Sidebar {
} => self.render_view_more(ix, path_list, *is_fully_expanded, is_selected, cx),
ListEntry::NewThread {
path_list,
- workspace,
- worktrees,
- } => self.render_new_thread(
- ix,
- path_list,
- workspace,
- is_active,
worktrees,
- is_selected,
- cx,
- ),
+ } => self.render_new_thread(ix, path_list, is_active, worktrees, is_selected, cx),
};
if is_group_header_after_first {
@@ -3058,8 +3052,7 @@ impl Sidebar {
fn render_new_thread(
&self,
ix: usize,
- _path_list: &PathList,
- workspace: &Entity<Workspace>,
+ path_list: &PathList,
is_active: bool,
worktrees: &[WorktreeInfo],
is_selected: bool,
@@ -3072,7 +3065,6 @@ impl Sidebar {
DEFAULT_THREAD_TITLE.into()
};
- let workspace = workspace.clone();
let id = SharedString::from(format!("new-thread-btn-{}", ix));
let thread_item = ThreadItem::new(id, label)
@@ -3093,7 +3085,7 @@ impl Sidebar {
.when(!is_active, |this| {
this.on_click(cx.listener(move |this, _, window, cx| {
this.selection = None;
- this.create_new_thread(&workspace, window, cx);
+ this.create_new_thread(&path_list, window, cx);
}))
});
@@ -8,13 +8,14 @@ use gpui::{
use project::DisableAiSettings;
#[cfg(any(test, feature = "test-support"))]
use project::Project;
+use remote::RemoteConnectionOptions;
use settings::Settings;
pub use settings::SidebarSide;
use std::future::Future;
use std::path::PathBuf;
use std::sync::Arc;
use ui::prelude::*;
-use util::ResultExt;
+use util::{ResultExt, path_list::PathList};
use zed_actions::agents_sidebar::{MoveWorkspaceToNewWindow, ToggleThreadSwitcher};
use agent_settings::AgentSettings;
@@ -230,6 +231,28 @@ pub struct MultiWorkspace {
_subscriptions: Vec<Subscription>,
}
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct ProjectGroupKey {
+ pub host: Option<RemoteConnectionOptions>,
+ pub main_worktree_paths: PathList,
+}
+
+impl ProjectGroupKey {
+ pub fn display_name(&self) -> SharedString {
+ let mut names = Vec::with_capacity(self.main_worktree_paths.paths().len());
+ for abs_path in self.main_worktree_paths.paths() {
+ if let Some(name) = abs_path.file_name() {
+ names.push(name.to_string_lossy().to_string());
+ }
+ }
+ if names.is_empty() {
+ "Empty Workspace".into()
+ } else {
+ names.join(", ").into()
+ }
+ }
+}
+
impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
impl MultiWorkspace {
@@ -454,6 +477,15 @@ impl MultiWorkspace {
&self.workspaces
}
+ pub fn project_group_keys(&self, cx: &App) -> impl Iterator<Item = ProjectGroupKey> {
+ let mut keys = Vec::new();
+ for workspace in &self.workspaces {
+ let key = workspace.read(cx).project_group_key(cx);
+ keys.push(key);
+ }
+ keys.into_iter()
+ }
+
pub fn active_workspace_index(&self) -> usize {
self.active_workspace_index
}
@@ -170,3 +170,8 @@ async fn test_replace(cx: &mut TestAppContext) {
);
});
}
+
+#[gpui::test]
+async fn test_worktrees(cx: &mut TestAppContext) {
+ //
+}
@@ -31,8 +31,9 @@ pub use crate::notifications::NotificationFrame;
pub use dock::Panel;
pub use multi_workspace::{
CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace,
- MultiWorkspaceEvent, NextWorkspace, PreviousWorkspace, Sidebar, SidebarEvent, SidebarHandle,
- SidebarRenderState, SidebarSide, ToggleWorkspaceSidebar, sidebar_side_context_menu,
+ MultiWorkspaceEvent, NextWorkspace, PreviousWorkspace, ProjectGroupKey, Sidebar, SidebarEvent,
+ SidebarHandle, SidebarRenderState, SidebarSide, ToggleWorkspaceSidebar,
+ sidebar_side_context_menu,
};
pub use path_list::{PathList, SerializedPathList};
pub use toast_layer::{ToastAction, ToastLayer, ToastView};
@@ -6375,6 +6376,30 @@ impl Workspace {
.collect::<Vec<_>>()
}
+ pub fn project_group_key(&self, cx: &App) -> ProjectGroupKey {
+ let project = self.project().read(cx);
+ let repositories = project.repositories(cx);
+ let paths = project
+ .visible_worktrees(cx)
+ .map(|worktree| {
+ let worktree = worktree.read(cx);
+ let main_worktree_path = repositories.values().find_map(|repo| {
+ let repo = repo.read(cx);
+ if repo.work_directory_abs_path == worktree.abs_path() {
+ Some(repo.original_repo_abs_path.clone())
+ } else {
+ None
+ }
+ });
+ main_worktree_path.unwrap_or_else(|| worktree.abs_path())
+ })
+ .collect::<Vec<_>>();
+ ProjectGroupKey {
+ host: project.remote_connection_options(cx),
+ main_worktree_paths: PathList::new(&paths),
+ }
+ }
+
fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
match member {
Member::Axis(PaneAxis { members, .. }) => {