Rework sidebar rendering to use MultiWorkspace's project groups (#53096)
Release Notes: * [x] It's possible to get into a state where agent panel shows a thread that is archived - N/A --------- Co-authored-by: Eric Holk <eric@zed.dev> Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
1 parent
c2478bff0d
commit
bce8cf86b5
8 files changed
+713
-922
No files matched your search
Generated
-1
@@ -15871,7 +15871,6 @@ dependencies = [
|
||||
"agent_ui",
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"collections",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
|
||||
@@ -2076,6 +2076,10 @@ impl AgentPanel {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(store) = ThreadMetadataStore::try_global(cx) {
|
||||
store.update(cx, |store, cx| store.unarchive(&session_id, cx));
|
||||
}
|
||||
|
||||
if let Some(conversation_view) = self.background_threads.remove(&session_id) {
|
||||
self.set_active_view(
|
||||
ActiveView::AgentThread { conversation_view },
|
||||
|
||||
@@ -6049,11 +6049,7 @@ impl ProjectGroupKey {
|
||||
/// Creates a new `ProjectGroupKey` with the given path list.
|
||||
///
|
||||
/// The path list should point to the git main worktree paths for a project.
|
||||
///
|
||||
/// This should be used only in a few places to make sure we can ensure the
|
||||
/// main worktree path invariant. Namely, this should only be called from
|
||||
/// [`Workspace`].
|
||||
pub(crate) fn new(host: Option<RemoteConnectionOptions>, paths: PathList) -> Self {
|
||||
pub fn new(host: Option<RemoteConnectionOptions>, paths: PathList) -> Self {
|
||||
Self { paths, host }
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ agent_settings.workspace = true
|
||||
agent_ui = { workspace = true, features = ["audio"] }
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
//! The sidebar groups threads by a canonical path list.
|
||||
//!
|
||||
//! Threads have a path list associated with them, but this is the absolute path
|
||||
//! of whatever worktrees they were associated with. In the sidebar, we want to
|
||||
//! group all threads by their main worktree, and then we add a worktree chip to
|
||||
//! the sidebar entry when that thread is in another worktree.
|
||||
//!
|
||||
//! This module is provides the functions and structures necessary to do this
|
||||
//! lookup and mapping.
|
||||
|
||||
use collections::{HashMap, HashSet, vecmap::VecMap};
|
||||
use gpui::{App, Entity};
|
||||
use project::ProjectGroupKey;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use workspace::{MultiWorkspace, PathList, Workspace};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ProjectGroup {
|
||||
pub workspaces: Vec<Entity<Workspace>>,
|
||||
/// Root paths of all open workspaces in this group. Used to skip
|
||||
/// redundant thread-store queries for linked worktrees that already
|
||||
/// have an open workspace.
|
||||
covered_paths: HashSet<Arc<Path>>,
|
||||
}
|
||||
|
||||
impl ProjectGroup {
|
||||
fn add_workspace(&mut self, workspace: &Entity<Workspace>, cx: &App) {
|
||||
if !self.workspaces.contains(workspace) {
|
||||
self.workspaces.push(workspace.clone());
|
||||
}
|
||||
for path in workspace.read(cx).root_paths(cx) {
|
||||
self.covered_paths.insert(path);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn first_workspace(&self) -> &Entity<Workspace> {
|
||||
self.workspaces
|
||||
.first()
|
||||
.expect("groups always have at least one workspace")
|
||||
}
|
||||
|
||||
pub fn main_workspace(&self, cx: &App) -> &Entity<Workspace> {
|
||||
self.workspaces
|
||||
.iter()
|
||||
.find(|ws| {
|
||||
!crate::root_repository_snapshots(ws, cx)
|
||||
.any(|snapshot| snapshot.is_linked_worktree())
|
||||
})
|
||||
.unwrap_or_else(|| self.first_workspace())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProjectGroupBuilder {
|
||||
/// Maps git repositories' work_directory_abs_path to their original_repo_abs_path
|
||||
directory_mappings: HashMap<PathBuf, PathBuf>,
|
||||
project_groups: VecMap<ProjectGroupKey, ProjectGroup>,
|
||||
}
|
||||
|
||||
impl ProjectGroupBuilder {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
directory_mappings: HashMap::default(),
|
||||
project_groups: VecMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_multiworkspace(mw: &MultiWorkspace, cx: &App) -> Self {
|
||||
let mut builder = Self::new();
|
||||
// First pass: collect all directory mappings from every workspace
|
||||
// so we know how to canonicalize any path (including linked
|
||||
// worktree paths discovered by the main repo's workspace).
|
||||
for workspace in mw.workspaces() {
|
||||
builder.add_workspace_mappings(workspace.read(cx), cx);
|
||||
}
|
||||
|
||||
// Second pass: group each workspace using canonical paths derived
|
||||
// from the full set of mappings.
|
||||
for workspace in mw.workspaces() {
|
||||
let group_name = workspace.read(cx).project_group_key(cx);
|
||||
builder
|
||||
.project_group_entry(&group_name)
|
||||
.add_workspace(workspace, cx);
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
fn project_group_entry(&mut self, name: &ProjectGroupKey) -> &mut ProjectGroup {
|
||||
self.project_groups.entry_ref(name).or_insert_default()
|
||||
}
|
||||
|
||||
fn add_mapping(&mut self, work_directory: &Path, original_repo: &Path) {
|
||||
let old = self
|
||||
.directory_mappings
|
||||
.insert(PathBuf::from(work_directory), PathBuf::from(original_repo));
|
||||
if let Some(old) = old {
|
||||
debug_assert_eq!(
|
||||
&old, original_repo,
|
||||
"all worktrees should map to the same main worktree"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_workspace_mappings(&mut self, workspace: &Workspace, cx: &App) {
|
||||
for repo in workspace.project().read(cx).repositories(cx).values() {
|
||||
let snapshot = repo.read(cx).snapshot();
|
||||
|
||||
self.add_mapping(
|
||||
&snapshot.work_directory_abs_path,
|
||||
&snapshot.original_repo_abs_path,
|
||||
);
|
||||
|
||||
for worktree in snapshot.linked_worktrees.iter() {
|
||||
self.add_mapping(&worktree.path, &snapshot.original_repo_abs_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn canonicalize_path<'a>(&'a self, path: &'a Path) -> &'a Path {
|
||||
self.directory_mappings
|
||||
.get(path)
|
||||
.map(AsRef::as_ref)
|
||||
.unwrap_or(path)
|
||||
}
|
||||
|
||||
/// Whether the given group should load threads for a linked worktree
|
||||
/// at `worktree_path`. Returns `false` if the worktree already has an
|
||||
/// open workspace in the group (its threads are loaded via the
|
||||
/// workspace loop) or if the worktree's canonical path list doesn't
|
||||
/// match `group_path_list`.
|
||||
pub fn group_owns_worktree(
|
||||
&self,
|
||||
group: &ProjectGroup,
|
||||
group_path_list: &PathList,
|
||||
worktree_path: &Path,
|
||||
) -> bool {
|
||||
if group.covered_paths.contains(worktree_path) {
|
||||
return false;
|
||||
}
|
||||
let canonical = self.canonicalize_path_list(&PathList::new(&[worktree_path]));
|
||||
canonical == *group_path_list
|
||||
}
|
||||
|
||||
/// Canonicalizes every path in a [`PathList`] using the builder's
|
||||
/// directory mappings.
|
||||
fn canonicalize_path_list(&self, path_list: &PathList) -> PathList {
|
||||
let paths: Vec<_> = path_list
|
||||
.paths()
|
||||
.iter()
|
||||
.map(|p| self.canonicalize_path(p).to_path_buf())
|
||||
.collect();
|
||||
PathList::new(&paths)
|
||||
}
|
||||
|
||||
pub fn groups(&self) -> impl Iterator<Item = (&ProjectGroupKey, &ProjectGroup)> {
|
||||
self.project_groups.iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use settings::SettingsStore;
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
theme_settings::init(theme::LoadThemes::JustBase, cx);
|
||||
});
|
||||
}
|
||||
|
||||
async fn create_fs_with_main_and_worktree(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
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.add_linked_worktree_for_repo(
|
||||
std::path::Path::new("/project/.git"),
|
||||
false,
|
||||
git::repository::Worktree {
|
||||
path: std::path::PathBuf::from("/wt/feature-a"),
|
||||
ref_name: Some("refs/heads/feature-a".into()),
|
||||
sha: "abc".into(),
|
||||
is_main: false,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
fs
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_main_repo_maps_to_itself(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = create_fs_with_main_and_worktree(cx).await;
|
||||
cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
|
||||
|
||||
let project = project::Project::test(fs.clone(), ["/project".as_ref()], cx).await;
|
||||
project
|
||||
.update(cx, |project, cx| project.git_scans_complete(cx))
|
||||
.await;
|
||||
|
||||
let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
|
||||
workspace::MultiWorkspace::test_new(project.clone(), window, cx)
|
||||
});
|
||||
|
||||
multi_workspace.read_with(cx, |mw, cx| {
|
||||
let mut canonicalizer = ProjectGroupBuilder::new();
|
||||
for workspace in mw.workspaces() {
|
||||
canonicalizer.add_workspace_mappings(workspace.read(cx), cx);
|
||||
}
|
||||
|
||||
// The main repo path should canonicalize to itself.
|
||||
assert_eq!(
|
||||
canonicalizer.canonicalize_path(Path::new("/project")),
|
||||
Path::new("/project"),
|
||||
);
|
||||
|
||||
// An unknown path returns None.
|
||||
assert_eq!(
|
||||
canonicalizer.canonicalize_path(Path::new("/something/else")),
|
||||
Path::new("/something/else"),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_worktree_checkout_canonicalizes_to_main_repo(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = create_fs_with_main_and_worktree(cx).await;
|
||||
cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
|
||||
|
||||
// Open the worktree checkout as its own project.
|
||||
let project = project::Project::test(fs.clone(), ["/wt/feature-a".as_ref()], cx).await;
|
||||
project
|
||||
.update(cx, |project, cx| project.git_scans_complete(cx))
|
||||
.await;
|
||||
|
||||
let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
|
||||
workspace::MultiWorkspace::test_new(project.clone(), window, cx)
|
||||
});
|
||||
|
||||
multi_workspace.read_with(cx, |mw, cx| {
|
||||
let mut canonicalizer = ProjectGroupBuilder::new();
|
||||
for workspace in mw.workspaces() {
|
||||
canonicalizer.add_workspace_mappings(workspace.read(cx), cx);
|
||||
}
|
||||
|
||||
// The worktree checkout path should canonicalize to the main repo.
|
||||
assert_eq!(
|
||||
canonicalizer.canonicalize_path(Path::new("/wt/feature-a")),
|
||||
Path::new("/project"),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+388
-351
File diff suppressed because it is too large.
Load diff
+300
-282
File diff suppressed because it is too large.
Load diff
@@ -474,6 +474,26 @@ impl MultiWorkspace {
|
||||
self.project_group_keys.iter()
|
||||
}
|
||||
|
||||
/// Returns the project groups, ordered by most recently added.
|
||||
pub fn project_groups(
|
||||
&self,
|
||||
cx: &App,
|
||||
) -> impl Iterator<Item = (ProjectGroupKey, Vec<Entity<Workspace>>)> {
|
||||
let mut groups = self
|
||||
.project_group_keys
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|key| (key.clone(), Vec::new()))
|
||||
.collect::<Vec<_>>();
|
||||
for workspace in &self.workspaces {
|
||||
let key = workspace.read(cx).project_group_key(cx);
|
||||
if let Some((_, workspaces)) = groups.iter_mut().find(|(k, _)| k == &key) {
|
||||
workspaces.push(workspace.clone());
|
||||
}
|
||||
}
|
||||
groups.into_iter()
|
||||
}
|
||||
|
||||
pub fn workspace(&self) -> &Entity<Workspace> {
|
||||
&self.workspaces[self.active_workspace_index]
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user