project-group-refactor.md

  1# ProjectGroup Refactor — Implementation Handoff
  2
  3## Goal
  4
  5Introduce `ProjectGroup` as an explicit entity that **owns** both its workspaces and its threads. Replace the current system where:
  6- Project groups are identified by `ProjectGroupKey` (derived from filesystem paths — structural identity)
  7- Thread-to-group association is derived at runtime via path matching across two HashMap indices
  8- Three parallel collections on `MultiWorkspace` are joined on every read
  9
 10With a system where:
 11- Each `ProjectGroup` has a stable `ProjectGroupId` (UUID)
 12- `ProjectGroup` directly contains its `Vec<Entity<Workspace>>`
 13- Threads store a `project_group_id: Option<ProjectGroupId>` for direct ownership
 14- `MultiWorkspace.active_workspace` is an independent `Entity<Workspace>` field (not an enum with index)
 15
 16## What already exists
 17
 18`ProjectGroupId` has already been added to `zed/crates/project/src/project.rs` (around L6120):
 19
 20```rust
 21#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)]
 22pub struct ProjectGroupId(uuid::Uuid);
 23
 24impl ProjectGroupId {
 25    pub fn new() -> Self {
 26        Self(uuid::Uuid::new_v4())
 27    }
 28}
 29```
 30
 31The `uuid` dependency has been added to `project/Cargo.toml`.
 32
 33---
 34
 35## File-by-file changes (in dependency order)
 36
 37### 1. `crates/project/src/project.rs`
 38
 39**Already done:** `ProjectGroupId` type exists.
 40
 41**No further changes needed** to this file. `ProjectGroupKey` stays as-is with path-based `Eq`/`Hash` — it's still useful as a computed descriptor for matching.
 42
 43### 2. `crates/workspace/src/persistence/model.rs`
 44
 45**Current state (lines 65–110):**
 46```rust
 47#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 48pub struct SerializedProjectGroupKey {
 49    pub path_list: SerializedPathList,
 50    pub(crate) location: SerializedWorkspaceLocation,
 51}
 52// From impls for ProjectGroupKey <-> SerializedProjectGroupKey
 53pub struct MultiWorkspaceState {
 54    pub active_workspace_id: Option<WorkspaceId>,
 55    pub sidebar_open: bool,
 56    pub project_group_keys: Vec<SerializedProjectGroupKey>,
 57    pub sidebar_state: Option<String>,
 58}
 59```
 60
 61**Changes:**
 62
 631. Rename `SerializedProjectGroupKey``SerializedProjectGroup` and add an `id` field:
 64```rust
 65#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 66pub struct SerializedProjectGroup {
 67    #[serde(default)] // absent in old blobs → None
 68    pub id: Option<ProjectGroupId>,
 69    pub path_list: SerializedPathList,
 70    pub(crate) location: SerializedWorkspaceLocation,
 71}
 72```
 73
 742. Update the `From` impls. The `From<ProjectGroupKey>` impl no longer makes sense because we need an ID. Instead, create a method or `From<(&ProjectGroupId, &ProjectGroupKey)>`:
 75```rust
 76impl SerializedProjectGroup {
 77    pub fn from_group(id: ProjectGroupId, key: &ProjectGroupKey) -> Self {
 78        Self {
 79            id: Some(id),
 80            path_list: key.path_list().serialize(),
 81            location: match key.host() {
 82                Some(host) => SerializedWorkspaceLocation::Remote(host),
 83                None => SerializedWorkspaceLocation::Local,
 84            },
 85        }
 86    }
 87
 88    pub fn to_key_and_id(self) -> (ProjectGroupId, ProjectGroupKey) {
 89        let id = self.id.unwrap_or_else(ProjectGroupId::new);
 90        let path_list = PathList::deserialize(&self.path_list);
 91        let host = match self.location {
 92            SerializedWorkspaceLocation::Local => None,
 93            SerializedWorkspaceLocation::Remote(opts) => Some(opts),
 94        };
 95        (id, ProjectGroupKey::new(host, path_list))
 96    }
 97}
 98```
 99
1003. Update `MultiWorkspaceState`:
101```rust
102pub struct MultiWorkspaceState {
103    pub active_workspace_id: Option<WorkspaceId>,
104    pub sidebar_open: bool,
105    pub project_group_keys: Vec<SerializedProjectGroup>, // renamed type
106    pub sidebar_state: Option<String>,
107}
108```
109
1104. Add `use project::ProjectGroupId;` to imports.
111
1125. The old `From<SerializedProjectGroupKey> for ProjectGroupKey` impl should be removed since callers now use `to_key_and_id()`.
113
114###