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###