Handle changing root paths without splitting in the sidebar (#53566)
Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #ISSUE Release Notes: - N/A --------- Co-authored-by: Eric Holk <eric@zed.dev>
This commit is contained in:
1 parent
3436e51042
commit
4a1fb25f67
6 files changed
+1347
-317
No files matched your search
@@ -477,6 +477,36 @@ impl ThreadMetadataStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_main_worktree_paths(
|
||||
&mut self,
|
||||
old_paths: &PathList,
|
||||
new_paths: PathList,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let session_ids = match self.threads_by_main_paths.remove(old_paths) {
|
||||
Some(ids) if !ids.is_empty() => ids,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let new_index = self
|
||||
.threads_by_main_paths
|
||||
.entry(new_paths.clone())
|
||||
.or_default();
|
||||
|
||||
for session_id in &session_ids {
|
||||
new_index.insert(session_id.clone());
|
||||
|
||||
if let Some(thread) = self.threads.get_mut(session_id) {
|
||||
thread.main_worktree_paths = new_paths.clone();
|
||||
self.pending_thread_ops_tx
|
||||
.try_send(DbOperation::Upsert(thread.clone()))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn create_archived_worktree(
|
||||
&self,
|
||||
worktree_path: String,
|
||||
|
||||
+132
-47
@@ -283,10 +283,8 @@ impl ListEntry {
|
||||
}
|
||||
}
|
||||
ListEntry::ProjectHeader { key, .. } => multi_workspace
|
||||
.workspaces()
|
||||
.find(|ws| PathList::new(&ws.read(cx).root_paths(cx)) == *key.path_list())
|
||||
.workspaces_for_project_group(key, cx)
|
||||
.cloned()
|
||||
.into_iter()
|
||||
.collect(),
|
||||
ListEntry::ViewMore { .. } => Vec::new(),
|
||||
}
|
||||
@@ -365,35 +363,81 @@ fn workspace_path_list(workspace: &Entity<Workspace>, cx: &App) -> PathList {
|
||||
///
|
||||
/// For each path in the thread's `folder_paths`, produces a
|
||||
/// [`WorktreeInfo`] with a short display name, full path, and whether
|
||||
/// the worktree is the main checkout or a linked git worktree.
|
||||
/// the worktree is the main checkout or a linked git worktree. When
|
||||
/// multiple main paths exist and a linked worktree's short name alone
|
||||
/// wouldn't identify which main project it belongs to, the main project
|
||||
/// name is prefixed for disambiguation (e.g. `project:feature`).
|
||||
///
|
||||
/// `linked_to_main` maps linked worktree abs paths to their main repo
|
||||
/// abs path, used to pick the correct prefix. Falls back to a heuristic
|
||||
/// when no mapping is available.
|
||||
fn worktree_info_from_thread_paths(
|
||||
folder_paths: &PathList,
|
||||
group_key: &project::ProjectGroupKey,
|
||||
) -> impl Iterator<Item = WorktreeInfo> {
|
||||
let main_paths = group_key.path_list().paths();
|
||||
folder_paths.paths().iter().filter_map(|path| {
|
||||
main_worktree_paths: &PathList,
|
||||
linked_to_main: &HashMap<PathBuf, PathBuf>,
|
||||
) -> Vec<WorktreeInfo> {
|
||||
let main_paths = main_worktree_paths.paths();
|
||||
|
||||
let mut infos: Vec<WorktreeInfo> = Vec::new();
|
||||
let mut linked_short_names: Vec<(SharedString, SharedString)> = Vec::new();
|
||||
|
||||
for path in folder_paths.paths().iter() {
|
||||
let is_main = main_paths.iter().any(|mp| mp.as_path() == path.as_path());
|
||||
if is_main {
|
||||
let name = path.file_name()?.to_string_lossy().to_string();
|
||||
Some(WorktreeInfo {
|
||||
name: SharedString::from(name),
|
||||
let Some(name) = path.file_name() else {
|
||||
continue;
|
||||
};
|
||||
infos.push(WorktreeInfo {
|
||||
name: SharedString::from(name.to_string_lossy().to_string()),
|
||||
full_path: SharedString::from(path.display().to_string()),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: ui::WorktreeKind::Main,
|
||||
})
|
||||
});
|
||||
} else {
|
||||
let main_path = main_paths
|
||||
.iter()
|
||||
.find(|mp| mp.file_name() == path.file_name())
|
||||
.or(main_paths.first())?;
|
||||
Some(WorktreeInfo {
|
||||
name: linked_worktree_short_name(main_path, path).unwrap_or_default(),
|
||||
let Some(main_path) = linked_to_main
|
||||
.get(&**path)
|
||||
.and_then(|main| main_paths.iter().find(|mp| mp.as_path() == main.as_path()))
|
||||
.or_else(|| {
|
||||
main_paths
|
||||
.iter()
|
||||
.find(|mp| mp.file_name() == path.file_name())
|
||||
.or(main_paths.first())
|
||||
})
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let short_name = linked_worktree_short_name(main_path, path).unwrap_or_default();
|
||||
let project_name = main_path
|
||||
.file_name()
|
||||
.map(|n| SharedString::from(n.to_string_lossy().to_string()))
|
||||
.unwrap_or_default();
|
||||
linked_short_names.push((short_name.clone(), project_name));
|
||||
infos.push(WorktreeInfo {
|
||||
name: short_name,
|
||||
full_path: SharedString::from(path.display().to_string()),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: ui::WorktreeKind::Linked,
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// When the group has multiple main worktree paths and the thread's
|
||||
// folder paths don't all share the same short name, prefix each
|
||||
// linked worktree chip with its main project name so the user knows
|
||||
// which project it belongs to.
|
||||
let all_same_name = infos.len() > 1 && infos.iter().all(|i| i.name == infos[0].name);
|
||||
|
||||
if main_paths.len() > 1 && !all_same_name {
|
||||
for (info, (_short_name, project_name)) in infos
|
||||
.iter_mut()
|
||||
.filter(|i| i.kind == ui::WorktreeKind::Linked)
|
||||
.zip(linked_short_names.iter())
|
||||
{
|
||||
info.name = SharedString::from(format!("{}:{}", project_name, info.name));
|
||||
}
|
||||
}
|
||||
|
||||
infos
|
||||
}
|
||||
|
||||
/// Shows a [`RemoteConnectionModal`] on the given workspace and establishes
|
||||
@@ -480,6 +524,16 @@ impl Sidebar {
|
||||
MultiWorkspaceEvent::WorkspaceRemoved(_) => {
|
||||
this.update_entries(cx);
|
||||
}
|
||||
MultiWorkspaceEvent::ProjectGroupKeyChanged { old_key, new_key } => {
|
||||
ThreadMetadataStore::global(cx).update(cx, |store, cx| {
|
||||
store.update_main_worktree_paths(
|
||||
old_key.path_list(),
|
||||
new_key.path_list().clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
this.update_entries(cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
@@ -912,6 +966,21 @@ impl Sidebar {
|
||||
.as_ref()
|
||||
.is_some_and(|active| group_workspaces.contains(active));
|
||||
|
||||
// Build a mapping from linked worktree paths to their main
|
||||
// repo path, used to correctly attribute chips.
|
||||
let linked_to_main: HashMap<PathBuf, PathBuf> = group_workspaces
|
||||
.iter()
|
||||
.flat_map(|ws| root_repository_snapshots(ws, cx))
|
||||
.flat_map(|snapshot| {
|
||||
let main_path = snapshot.original_repo_abs_path.to_path_buf();
|
||||
snapshot
|
||||
.linked_worktrees()
|
||||
.iter()
|
||||
.map(move |wt| (wt.path.clone(), main_path.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Collect live thread infos from all workspaces in this group.
|
||||
let live_infos: Vec<_> = group_workspaces
|
||||
.iter()
|
||||
@@ -949,26 +1018,28 @@ impl Sidebar {
|
||||
};
|
||||
|
||||
// Build a ThreadEntry from a metadata row.
|
||||
let make_thread_entry = |row: ThreadMetadata,
|
||||
workspace: ThreadEntryWorkspace|
|
||||
-> ThreadEntry {
|
||||
let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id);
|
||||
let worktrees: Vec<WorktreeInfo> =
|
||||
worktree_info_from_thread_paths(&row.folder_paths, &group_key).collect();
|
||||
ThreadEntry {
|
||||
metadata: row,
|
||||
icon,
|
||||
icon_from_external_svg,
|
||||
status: AgentThreadStatus::default(),
|
||||
workspace,
|
||||
is_live: false,
|
||||
is_background: false,
|
||||
is_title_generating: false,
|
||||
highlight_positions: Vec::new(),
|
||||
worktrees,
|
||||
diff_stats: DiffStats::default(),
|
||||
}
|
||||
};
|
||||
let make_thread_entry =
|
||||
|row: ThreadMetadata, workspace: ThreadEntryWorkspace| -> ThreadEntry {
|
||||
let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id);
|
||||
let worktrees = worktree_info_from_thread_paths(
|
||||
&row.folder_paths,
|
||||
&row.main_worktree_paths,
|
||||
&linked_to_main,
|
||||
);
|
||||
ThreadEntry {
|
||||
metadata: row,
|
||||
icon,
|
||||
icon_from_external_svg,
|
||||
status: AgentThreadStatus::default(),
|
||||
workspace,
|
||||
is_live: false,
|
||||
is_background: false,
|
||||
is_title_generating: false,
|
||||
highlight_positions: Vec::new(),
|
||||
worktrees,
|
||||
diff_stats: DiffStats::default(),
|
||||
}
|
||||
};
|
||||
|
||||
// Main code path: one query per group via main_worktree_paths.
|
||||
// The main_worktree_paths column is set on all new threads and
|
||||
@@ -1184,11 +1255,17 @@ impl Sidebar {
|
||||
if is_draft_for_group {
|
||||
if let Some(ActiveEntry::Draft(draft_ws)) = &self.active_entry {
|
||||
let ws_path_list = workspace_path_list(draft_ws, cx);
|
||||
let worktrees = worktree_info_from_thread_paths(&ws_path_list, &group_key);
|
||||
let main_worktree_paths =
|
||||
draft_ws.read(cx).project_group_key(cx).path_list().clone();
|
||||
let worktrees = worktree_info_from_thread_paths(
|
||||
&ws_path_list,
|
||||
&main_worktree_paths,
|
||||
&linked_to_main,
|
||||
);
|
||||
entries.push(ListEntry::DraftThread {
|
||||
key: group_key.clone(),
|
||||
workspace: None,
|
||||
worktrees: worktrees.collect(),
|
||||
worktrees,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1212,9 +1289,14 @@ impl Sidebar {
|
||||
continue;
|
||||
}
|
||||
let ws_path_list = workspace_path_list(ws, cx);
|
||||
let has_linked_worktrees =
|
||||
worktree_info_from_thread_paths(&ws_path_list, &group_key)
|
||||
.any(|wt| wt.kind == ui::WorktreeKind::Linked);
|
||||
let ws_main_paths = ws.read(cx).project_group_key(cx).path_list().clone();
|
||||
let has_linked_worktrees = worktree_info_from_thread_paths(
|
||||
&ws_path_list,
|
||||
&ws_main_paths,
|
||||
&linked_to_main,
|
||||
)
|
||||
.iter()
|
||||
.any(|wt| wt.kind == ui::WorktreeKind::Linked);
|
||||
if !has_linked_worktrees {
|
||||
continue;
|
||||
}
|
||||
@@ -1227,8 +1309,11 @@ impl Sidebar {
|
||||
if has_threads {
|
||||
continue;
|
||||
}
|
||||
let worktrees: Vec<WorktreeInfo> =
|
||||
worktree_info_from_thread_paths(&ws_path_list, &group_key).collect();
|
||||
let worktrees = worktree_info_from_thread_paths(
|
||||
&ws_path_list,
|
||||
&ws_main_paths,
|
||||
&linked_to_main,
|
||||
);
|
||||
|
||||
entries.push(ListEntry::DraftThread {
|
||||
key: group_key.clone(),
|
||||
|
||||
+1018
-72
File diff suppressed because it is too large.
Load diff
@@ -101,6 +101,10 @@ pub enum MultiWorkspaceEvent {
|
||||
ActiveWorkspaceChanged,
|
||||
WorkspaceAdded(Entity<Workspace>),
|
||||
WorkspaceRemoved(EntityId),
|
||||
ProjectGroupKeyChanged {
|
||||
old_key: ProjectGroupKey,
|
||||
new_key: ProjectGroupKey,
|
||||
},
|
||||
}
|
||||
|
||||
pub enum SidebarEvent {
|
||||
@@ -302,7 +306,7 @@ pub struct MultiWorkspace {
|
||||
workspaces: Vec<Entity<Workspace>>,
|
||||
active_workspace: ActiveWorkspace,
|
||||
project_group_keys: Vec<ProjectGroupKey>,
|
||||
provisional_project_group_keys: HashMap<EntityId, ProjectGroupKey>,
|
||||
workspace_group_keys: HashMap<EntityId, ProjectGroupKey>,
|
||||
sidebar: Option<Box<dyn SidebarHandle>>,
|
||||
sidebar_open: bool,
|
||||
sidebar_overlay: Option<AnyView>,
|
||||
@@ -355,7 +359,7 @@ impl MultiWorkspace {
|
||||
Self {
|
||||
window_id: window.window_handle().window_id(),
|
||||
project_group_keys: Vec::new(),
|
||||
provisional_project_group_keys: HashMap::default(),
|
||||
workspace_group_keys: HashMap::default(),
|
||||
workspaces: Vec::new(),
|
||||
active_workspace: ActiveWorkspace::Transient(workspace),
|
||||
sidebar: None,
|
||||
@@ -559,19 +563,11 @@ impl MultiWorkspace {
|
||||
cx.subscribe_in(&project, window, {
|
||||
let workspace = workspace.downgrade();
|
||||
move |this, _project, event, _window, cx| match event {
|
||||
project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => {
|
||||
project::Event::WorktreeAdded(_)
|
||||
| project::Event::WorktreeRemoved(_)
|
||||
| project::Event::WorktreeUpdatedRootRepoCommonDir(_) => {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
this.add_project_group_key(workspace.read(cx).project_group_key(cx));
|
||||
}
|
||||
}
|
||||
project::Event::WorktreeUpdatedRootRepoCommonDir(_) => {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
this.maybe_clear_provisional_project_group_key(&workspace, cx);
|
||||
this.add_project_group_key(
|
||||
this.project_group_key_for_workspace(&workspace, cx),
|
||||
);
|
||||
this.remove_stale_project_group_keys(cx);
|
||||
cx.notify();
|
||||
this.handle_workspace_key_change(&workspace, cx);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -587,7 +583,111 @@ impl MultiWorkspace {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn add_project_group_key(&mut self, project_group_key: ProjectGroupKey) {
|
||||
fn handle_workspace_key_change(
|
||||
&mut self,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let workspace_id = workspace.entity_id();
|
||||
let old_key = self.project_group_key_for_workspace(workspace, cx);
|
||||
let new_key = workspace.read(cx).project_group_key(cx);
|
||||
|
||||
if new_key.path_list().paths().is_empty() || old_key == new_key {
|
||||
return;
|
||||
}
|
||||
|
||||
let active_workspace = self.workspace().clone();
|
||||
|
||||
self.set_workspace_group_key(workspace, new_key.clone());
|
||||
|
||||
let changed_root_paths = workspace.read(cx).root_paths(cx);
|
||||
let old_paths = old_key.path_list().paths();
|
||||
let new_paths = new_key.path_list().paths();
|
||||
|
||||
// Remove workspaces that already had the new key and have the same
|
||||
// root paths (true duplicates that this workspace is replacing).
|
||||
//
|
||||
// NOTE: These are dropped without prompting for unsaved changes because
|
||||
// the user explicitly added a folder that makes this workspace
|
||||
// identical to the duplicate — they are intentionally overwriting it.
|
||||
let duplicate_workspaces: Vec<Entity<Workspace>> = self
|
||||
.workspaces
|
||||
.iter()
|
||||
.filter(|ws| {
|
||||
ws.entity_id() != workspace_id
|
||||
&& self.project_group_key_for_workspace(ws, cx) == new_key
|
||||
&& ws.read(cx).root_paths(cx) == changed_root_paths
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if duplicate_workspaces.contains(&active_workspace) {
|
||||
// The active workspace is among the duplicates — drop the
|
||||
// incoming workspace instead so the user stays where they are.
|
||||
self.detach_workspace(workspace, cx);
|
||||
self.workspaces.retain(|w| w != workspace);
|
||||
} else {
|
||||
for ws in &duplicate_workspaces {
|
||||
self.detach_workspace(ws, cx);
|
||||
self.workspaces.retain(|w| w != ws);
|
||||
}
|
||||
}
|
||||
|
||||
// Propagate folder adds/removes to linked worktree siblings
|
||||
// (different root paths, same old key) so they stay in the group.
|
||||
let group_workspaces: Vec<Entity<Workspace>> = self
|
||||
.workspaces
|
||||
.iter()
|
||||
.filter(|ws| {
|
||||
ws.entity_id() != workspace_id
|
||||
&& self.project_group_key_for_workspace(ws, cx) == old_key
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
for workspace in &group_workspaces {
|
||||
// Pre-set this to stop later WorktreeAdded events from triggering
|
||||
self.set_workspace_group_key(&workspace, new_key.clone());
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
|
||||
for added_path in new_paths.iter().filter(|p| !old_paths.contains(p)) {
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree(added_path, true, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
for removed_path in old_paths.iter().filter(|p| !new_paths.contains(p)) {
|
||||
project.update(cx, |project, cx| {
|
||||
project.remove_worktree_for_main_worktree_path(removed_path, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the active workspace after removals may have shifted
|
||||
// the index. If the previously active workspace was removed,
|
||||
// fall back to the workspace whose key just changed.
|
||||
if let ActiveWorkspace::Persistent(_) = &self.active_workspace {
|
||||
let target = if self.workspaces.contains(&active_workspace) {
|
||||
&active_workspace
|
||||
} else {
|
||||
workspace
|
||||
};
|
||||
if let Some(new_index) = self.workspaces.iter().position(|ws| ws == target) {
|
||||
self.active_workspace = ActiveWorkspace::Persistent(new_index);
|
||||
}
|
||||
}
|
||||
|
||||
self.remove_stale_project_group_keys(cx);
|
||||
|
||||
cx.emit(MultiWorkspaceEvent::ProjectGroupKeyChanged { old_key, new_key });
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn add_project_group_key(&mut self, project_group_key: ProjectGroupKey) {
|
||||
if project_group_key.path_list().paths().is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -598,12 +698,12 @@ impl MultiWorkspace {
|
||||
self.project_group_keys.insert(0, project_group_key);
|
||||
}
|
||||
|
||||
pub fn set_provisional_project_group_key(
|
||||
pub(crate) fn set_workspace_group_key(
|
||||
&mut self,
|
||||
workspace: &Entity<Workspace>,
|
||||
project_group_key: ProjectGroupKey,
|
||||
) {
|
||||
self.provisional_project_group_keys
|
||||
self.workspace_group_keys
|
||||
.insert(workspace.entity_id(), project_group_key.clone());
|
||||
self.add_project_group_key(project_group_key);
|
||||
}
|
||||
@@ -613,28 +713,12 @@ impl MultiWorkspace {
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &App,
|
||||
) -> ProjectGroupKey {
|
||||
self.provisional_project_group_keys
|
||||
self.workspace_group_keys
|
||||
.get(&workspace.entity_id())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| workspace.read(cx).project_group_key(cx))
|
||||
}
|
||||
|
||||
fn maybe_clear_provisional_project_group_key(
|
||||
&mut self,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &App,
|
||||
) {
|
||||
let live_key = workspace.read(cx).project_group_key(cx);
|
||||
if self
|
||||
.provisional_project_group_keys
|
||||
.get(&workspace.entity_id())
|
||||
.is_some_and(|key| *key == live_key)
|
||||
{
|
||||
self.provisional_project_group_keys
|
||||
.remove(&workspace.entity_id());
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_stale_project_group_keys(&mut self, cx: &App) {
|
||||
let workspace_keys: HashSet<ProjectGroupKey> = self
|
||||
.workspaces
|
||||
@@ -1045,7 +1129,6 @@ impl MultiWorkspace {
|
||||
self.promote_transient(old, cx);
|
||||
} else {
|
||||
self.detach_workspace(&old, cx);
|
||||
cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(old.entity_id()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1056,7 +1139,6 @@ impl MultiWorkspace {
|
||||
});
|
||||
if let Some(old) = self.active_workspace.set_transient(workspace) {
|
||||
self.detach_workspace(&old, cx);
|
||||
cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(old.entity_id()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1083,7 +1165,7 @@ impl MultiWorkspace {
|
||||
/// Returns the index of the newly inserted workspace.
|
||||
fn promote_transient(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) -> usize {
|
||||
let project_group_key = self.project_group_key_for_workspace(&workspace, cx);
|
||||
self.add_project_group_key(project_group_key);
|
||||
self.set_workspace_group_key(&workspace, project_group_key);
|
||||
self.workspaces.push(workspace.clone());
|
||||
cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
|
||||
self.workspaces.len() - 1
|
||||
@@ -1099,10 +1181,10 @@ impl MultiWorkspace {
|
||||
for workspace in std::mem::take(&mut self.workspaces) {
|
||||
if workspace != active {
|
||||
self.detach_workspace(&workspace, cx);
|
||||
cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
|
||||
}
|
||||
}
|
||||
self.project_group_keys.clear();
|
||||
self.workspace_group_keys.clear();
|
||||
self.active_workspace = ActiveWorkspace::Transient(active);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1128,7 +1210,7 @@ impl MultiWorkspace {
|
||||
workspace.set_multi_workspace(weak_self, cx);
|
||||
});
|
||||
|
||||
self.add_project_group_key(project_group_key);
|
||||
self.set_workspace_group_key(&workspace, project_group_key);
|
||||
self.workspaces.push(workspace.clone());
|
||||
cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
|
||||
cx.notify();
|
||||
@@ -1136,10 +1218,12 @@ impl MultiWorkspace {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears session state and DB binding for a workspace that is being
|
||||
/// removed or replaced. The DB row is preserved so the workspace still
|
||||
/// appears in the recent-projects list.
|
||||
/// Detaches a workspace: clears session state, DB binding, cached
|
||||
/// group key, and emits `WorkspaceRemoved`. The DB row is preserved
|
||||
/// so the workspace still appears in the recent-projects list.
|
||||
fn detach_workspace(&mut self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
|
||||
self.workspace_group_keys.remove(&workspace.entity_id());
|
||||
cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
|
||||
workspace.update(cx, |workspace, _cx| {
|
||||
workspace.session_id.take();
|
||||
workspace._schedule_serialize_workspace.take();
|
||||
@@ -1313,6 +1397,46 @@ impl MultiWorkspace {
|
||||
tasks
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn assert_project_group_key_integrity(&self, cx: &App) -> anyhow::Result<()> {
|
||||
let stored_keys: HashSet<&ProjectGroupKey> = self.project_group_keys().collect();
|
||||
|
||||
let workspace_group_keys: HashSet<&ProjectGroupKey> =
|
||||
self.workspace_group_keys.values().collect();
|
||||
let extra_keys = &workspace_group_keys - &stored_keys;
|
||||
anyhow::ensure!(
|
||||
extra_keys.is_empty(),
|
||||
"workspace_group_keys values not in project_group_keys: {:?}",
|
||||
extra_keys,
|
||||
);
|
||||
|
||||
let cached_ids: HashSet<EntityId> = self.workspace_group_keys.keys().copied().collect();
|
||||
let workspace_ids: HashSet<EntityId> =
|
||||
self.workspaces.iter().map(|ws| ws.entity_id()).collect();
|
||||
anyhow::ensure!(
|
||||
cached_ids == workspace_ids,
|
||||
"workspace_group_keys entity IDs don't match workspaces.\n\
|
||||
only in cache: {:?}\n\
|
||||
only in workspaces: {:?}",
|
||||
&cached_ids - &workspace_ids,
|
||||
&workspace_ids - &cached_ids,
|
||||
);
|
||||
|
||||
for workspace in self.workspaces() {
|
||||
let live_key = workspace.read(cx).project_group_key(cx);
|
||||
let cached_key = &self.workspace_group_keys[&workspace.entity_id()];
|
||||
anyhow::ensure!(
|
||||
*cached_key == live_key,
|
||||
"workspace {:?} has live key {:?} but cached key {:?}",
|
||||
workspace.entity_id(),
|
||||
live_key,
|
||||
cached_key,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
|
||||
self.workspace().update(cx, |workspace, _cx| {
|
||||
@@ -1471,7 +1595,6 @@ impl MultiWorkspace {
|
||||
|
||||
for workspace in &removed_workspaces {
|
||||
this.detach_workspace(workspace, cx);
|
||||
cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
|
||||
}
|
||||
|
||||
let removed_any = !removed_workspaces.is_empty();
|
||||
|
||||
@@ -185,157 +185,3 @@ async fn test_project_group_keys_duplicate_not_added(cx: &mut TestAppContext) {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_project_group_keys_on_worktree_added(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
|
||||
fs.insert_tree("/root_b", json!({ "file.txt": "" })).await;
|
||||
let project = Project::test(fs, ["/root_a".as_ref()], cx).await;
|
||||
|
||||
let initial_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
|
||||
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
|
||||
multi_workspace.update(cx, |mw, cx| {
|
||||
mw.open_sidebar(cx);
|
||||
});
|
||||
|
||||
// Add a second worktree to the same project.
|
||||
let (worktree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree("/root_b", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
worktree
|
||||
.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let updated_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
|
||||
assert_ne!(
|
||||
initial_key, updated_key,
|
||||
"key should change after adding a worktree"
|
||||
);
|
||||
|
||||
multi_workspace.read_with(cx, |mw, _cx| {
|
||||
let keys: Vec<&ProjectGroupKey> = mw.project_group_keys().collect();
|
||||
assert_eq!(
|
||||
keys.len(),
|
||||
2,
|
||||
"should have both the original and updated key"
|
||||
);
|
||||
assert_eq!(*keys[0], updated_key);
|
||||
assert_eq!(*keys[1], initial_key);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_project_group_keys_on_worktree_removed(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
|
||||
fs.insert_tree("/root_b", json!({ "file.txt": "" })).await;
|
||||
let project = Project::test(fs, ["/root_a".as_ref(), "/root_b".as_ref()], cx).await;
|
||||
|
||||
let initial_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
|
||||
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
|
||||
multi_workspace.update(cx, |mw, cx| {
|
||||
mw.open_sidebar(cx);
|
||||
});
|
||||
|
||||
// Remove one worktree.
|
||||
let worktree_b_id = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees(cx)
|
||||
.find(|wt| wt.read(cx).root_name().as_unix_str() == "root_b")
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.id()
|
||||
});
|
||||
project.update(cx, |project, cx| {
|
||||
project.remove_worktree(worktree_b_id, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let updated_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
|
||||
assert_ne!(
|
||||
initial_key, updated_key,
|
||||
"key should change after removing a worktree"
|
||||
);
|
||||
|
||||
multi_workspace.read_with(cx, |mw, _cx| {
|
||||
let keys: Vec<&ProjectGroupKey> = mw.project_group_keys().collect();
|
||||
assert_eq!(
|
||||
keys.len(),
|
||||
2,
|
||||
"should accumulate both the original and post-removal key"
|
||||
);
|
||||
assert_eq!(*keys[0], updated_key);
|
||||
assert_eq!(*keys[1], initial_key);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_project_group_keys_across_multiple_workspaces_and_worktree_changes(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
|
||||
fs.insert_tree("/root_b", json!({ "file.txt": "" })).await;
|
||||
fs.insert_tree("/root_c", json!({ "file.txt": "" })).await;
|
||||
let project_a = Project::test(fs.clone(), ["/root_a".as_ref()], cx).await;
|
||||
let project_b = Project::test(fs.clone(), ["/root_b".as_ref()], cx).await;
|
||||
|
||||
let key_a = project_a.read_with(cx, |p, cx| p.project_group_key(cx));
|
||||
let key_b = project_b.read_with(cx, |p, cx| p.project_group_key(cx));
|
||||
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
|
||||
|
||||
multi_workspace.update(cx, |mw, cx| {
|
||||
mw.open_sidebar(cx);
|
||||
});
|
||||
|
||||
multi_workspace.update_in(cx, |mw, window, cx| {
|
||||
mw.test_add_workspace(project_b, window, cx);
|
||||
});
|
||||
|
||||
multi_workspace.read_with(cx, |mw, _cx| {
|
||||
assert_eq!(mw.project_group_keys().count(), 2);
|
||||
});
|
||||
|
||||
// Now add a worktree to project_a. This should produce a third key.
|
||||
let (worktree, _) = project_a
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree("/root_c", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
worktree
|
||||
.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let key_a_updated = project_a.read_with(cx, |p, cx| p.project_group_key(cx));
|
||||
assert_ne!(key_a, key_a_updated);
|
||||
|
||||
multi_workspace.read_with(cx, |mw, _cx| {
|
||||
let keys: Vec<&ProjectGroupKey> = mw.project_group_keys().collect();
|
||||
assert_eq!(
|
||||
keys.len(),
|
||||
3,
|
||||
"should have key_a, key_b, and the updated key_a with root_c"
|
||||
);
|
||||
assert_eq!(*keys[0], key_a_updated);
|
||||
assert_eq!(*keys[1], key_b);
|
||||
assert_eq!(*keys[2], key_a);
|
||||
});
|
||||
}
|
||||
@@ -9886,7 +9886,7 @@ async fn open_remote_project_inner(
|
||||
});
|
||||
|
||||
if let Some(project_group_key) = provisional_project_group_key.clone() {
|
||||
multi_workspace.set_provisional_project_group_key(&new_workspace, project_group_key);
|
||||
multi_workspace.set_workspace_group_key(&new_workspace, project_group_key);
|
||||
}
|
||||
multi_workspace.activate(new_workspace.clone(), window, cx);
|
||||
new_workspace
|
||||
|
||||
Reference in new issue
Block a user