thread_worktree_archive.rs

  1use std::{
  2    path::{Path, PathBuf},
  3    sync::Arc,
  4};
  5
  6use agent_client_protocol as acp;
  7use anyhow::{Context as _, Result, anyhow};
  8use gpui::{App, AsyncApp, Entity, Task};
  9use project::{
 10    LocalProjectFlags, Project, WorktreeId,
 11    git_store::{Repository, resolve_git_worktree_to_main_repo},
 12};
 13use util::ResultExt;
 14use workspace::{AppState, MultiWorkspace, Workspace};
 15
 16use crate::thread_metadata_store::{ArchivedGitWorktree, ThreadMetadataStore};
 17
 18/// The plan for archiving a single git worktree root.
 19///
 20/// A thread can have multiple folder paths open, so there may be multiple
 21/// `RootPlan`s per archival operation. Each one captures everything needed to
 22/// persist the worktree's git state and then remove it from disk.
 23///
 24/// All fields are gathered synchronously by [`build_root_plan`] while the
 25/// worktree is still loaded in open projects. This is important because
 26/// workspace removal tears down project and repository entities, making
 27/// them unavailable for the later async persist/remove steps.
 28#[derive(Clone)]
 29pub struct RootPlan {
 30    /// Absolute path of the git worktree on disk.
 31    pub root_path: PathBuf,
 32    /// Absolute path to the main git repository this worktree is linked to.
 33    /// Used both for creating a git ref to prevent GC of WIP commits during
 34    /// [`persist_worktree_state`], and for `git worktree remove` during
 35    /// [`remove_root`].
 36    pub main_repo_path: PathBuf,
 37    /// Every open `Project` that has this worktree loaded, so they can all
 38    /// call `remove_worktree` and release it during [`remove_root`].
 39    /// Multiple projects can reference the same path when the user has the
 40    /// worktree open in more than one workspace.
 41    pub affected_projects: Vec<AffectedProject>,
 42    /// The `Repository` entity for this linked worktree, used to run git
 43    /// commands (create WIP commits, stage files, reset) during
 44    /// [`persist_worktree_state`].
 45    pub worktree_repo: Entity<Repository>,
 46    /// The branch the worktree was on, so it can be restored later.
 47    /// `None` if the worktree was in detached HEAD state.
 48    pub branch_name: Option<String>,
 49}
 50
 51/// A `Project` that references a worktree being archived, paired with the
 52/// `WorktreeId` it uses for that worktree.
 53///
 54/// The same worktree path can appear in multiple open workspaces/projects
 55/// (e.g. when the user has two windows open that both include the same
 56/// linked worktree). Each one needs to call `remove_worktree` and wait for
 57/// the release during [`remove_root`], otherwise the project would still
 58/// hold a reference to the directory and `git worktree remove` would fail.
 59#[derive(Clone)]
 60pub struct AffectedProject {
 61    pub project: Entity<Project>,
 62    pub worktree_id: WorktreeId,
 63}
 64
 65fn archived_worktree_ref_name(id: i64) -> String {
 66    format!("refs/archived-worktrees/{}", id)
 67}
 68
 69/// Builds a [`RootPlan`] for archiving the git worktree at `path`.
 70///
 71/// This is a synchronous planning step that must run *before* any workspace
 72/// removal, because it needs live project and repository entities that are
 73/// torn down when a workspace is removed. It does three things:
 74///
 75/// 1. Finds every `Project` across all open workspaces that has this
 76///    worktree loaded (`affected_projects`).
 77/// 2. Looks for a `Repository` entity whose snapshot identifies this path
 78///    as a linked worktree (`worktree_repo`), which is needed for the git
 79///    operations in [`persist_worktree_state`].
 80/// 3. Determines the `main_repo_path` — the parent repo that owns this
 81///    linked worktree — needed for both git ref creation and
 82///    `git worktree remove`.
 83///
 84/// Returns `None` if the path is not a linked worktree (main worktrees
 85/// cannot be archived to disk) or if no open project has it loaded.
 86pub fn build_root_plan(
 87    path: &Path,
 88    workspaces: &[Entity<Workspace>],
 89    cx: &App,
 90) -> Option<RootPlan> {
 91    let path = path.to_path_buf();
 92
 93    let affected_projects = workspaces
 94        .iter()
 95        .filter_map(|workspace| {
 96            let project = workspace.read(cx).project().clone();
 97            let worktree = project
 98                .read(cx)
 99                .visible_worktrees(cx)
100                .find(|worktree| worktree.read(cx).abs_path().as_ref() == path.as_path())?;
101            let worktree_id = worktree.read(cx).id();
102            Some(AffectedProject {
103                project,
104                worktree_id,
105            })
106        })
107        .collect::<Vec<_>>();
108
109    if affected_projects.is_empty() {
110        return None;
111    }
112
113    let linked_repo = workspaces
114        .iter()
115        .flat_map(|workspace| {
116            workspace
117                .read(cx)
118                .project()
119                .read(cx)
120                .repositories(cx)
121                .values()
122                .cloned()
123                .collect::<Vec<_>>()
124        })
125        .find_map(|repo| {
126            let snapshot = repo.read(cx).snapshot();
127            (snapshot.is_linked_worktree()
128                && snapshot.work_directory_abs_path.as_ref() == path.as_path())
129            .then_some((snapshot, repo))
130        });
131
132    // Only linked worktrees can be archived to disk via `git worktree remove`.
133    // Main worktrees must be left alone — git refuses to remove them.
134    let (linked_snapshot, repo) = linked_repo?;
135    let main_repo_path = linked_snapshot.original_repo_abs_path.to_path_buf();
136    let branch_name = linked_snapshot
137        .branch
138        .as_ref()
139        .map(|branch| branch.name().to_string());
140    Some(RootPlan {
141        root_path: path,
142        main_repo_path,
143        affected_projects,
144        worktree_repo: repo,
145        branch_name,
146    })
147}
148
149/// Returns `true` if any unarchived thread other than `current_session_id`
150/// references `path` in its folder paths. Used to determine whether a
151/// worktree can safely be removed from disk.
152pub fn path_is_referenced_by_other_unarchived_threads(
153    current_session_id: &acp::SessionId,
154    path: &Path,
155    cx: &App,
156) -> bool {
157    ThreadMetadataStore::global(cx)
158        .read(cx)
159        .entries()
160        .filter(|thread| thread.session_id != *current_session_id)
161        .filter(|thread| !thread.archived)
162        .any(|thread| {
163            thread
164                .folder_paths
165                .paths()
166                .iter()
167                .any(|other_path| other_path.as_path() == path)
168        })
169}
170
171/// Removes a worktree from all affected projects and deletes it from disk
172/// via `git worktree remove`.
173///
174/// This is the destructive counterpart to [`persist_worktree_state`]. It
175/// first detaches the worktree from every [`AffectedProject`], waits for
176/// each project to fully release it, then asks the main repository to
177/// delete the worktree directory. If the git removal fails, the worktree
178/// is re-added to each project via [`rollback_root`].
179pub async fn remove_root(root: RootPlan, cx: &mut AsyncApp) -> Result<()> {
180    let release_tasks: Vec<_> = root
181        .affected_projects
182        .iter()
183        .map(|affected| {
184            let project = affected.project.clone();
185            let worktree_id = affected.worktree_id;
186            project.update(cx, |project, cx| {
187                let wait = project.wait_for_worktree_release(worktree_id, cx);
188                project.remove_worktree(worktree_id, cx);
189                wait
190            })
191        })
192        .collect();
193
194    if let Err(error) = remove_root_after_worktree_removal(&root, release_tasks, cx).await {
195        rollback_root(&root, cx).await;
196        return Err(error);
197    }
198
199    Ok(())
200}
201
202async fn remove_root_after_worktree_removal(
203    root: &RootPlan,
204    release_tasks: Vec<Task<Result<()>>>,
205    cx: &mut AsyncApp,
206) -> Result<()> {
207    for task in release_tasks {
208        if let Err(error) = task.await {
209            log::error!("Failed waiting for worktree release: {error:#}");
210        }
211    }
212
213    let (repo, _temp_project) = find_or_create_repository(&root.main_repo_path, cx).await?;
214    // force=true is required because the working directory is still dirty
215    // — persist_worktree_state captures state into detached commits without
216    // modifying the real index or working tree, so git refuses to delete
217    // the worktree without --force.
218    let receiver = repo.update(cx, |repo: &mut Repository, _cx| {
219        repo.remove_worktree(root.root_path.clone(), true)
220    });
221    let result = receiver
222        .await
223        .map_err(|_| anyhow!("git worktree removal was canceled"))?;
224    // Keep _temp_project alive until after the await so the headless project isn't dropped mid-operation
225    drop(_temp_project);
226    result
227}
228
229/// Finds a live `Repository` entity for the given path, or creates a temporary
230/// `Project::local` to obtain one.
231///
232/// `Repository` entities can only be obtained through a `Project` because
233/// `GitStore` (which creates and manages `Repository` entities) is owned by
234/// `Project`. When no open workspace contains the repo we need, we spin up a
235/// headless `Project::local` just to get a `Repository` handle. The caller
236/// keeps the returned `Option<Entity<Project>>` alive for the duration of the
237/// git operations, then drops it.
238///
239/// Future improvement: decoupling `GitStore` from `Project` so that
240/// `Repository` entities can be created standalone would eliminate this
241/// temporary-project workaround.
242async fn find_or_create_repository(
243    repo_path: &Path,
244    cx: &mut AsyncApp,
245) -> Result<(Entity<Repository>, Option<Entity<Project>>)> {
246    let repo_path_owned = repo_path.to_path_buf();
247    let live_repo = cx.update(|cx| {
248        all_open_workspaces(cx)
249            .into_iter()
250            .flat_map(|workspace| {
251                workspace
252                    .read(cx)
253                    .project()
254                    .read(cx)
255                    .repositories(cx)
256                    .values()
257                    .cloned()
258                    .collect::<Vec<_>>()
259            })
260            .find(|repo| {
261                repo.read(cx).snapshot().work_directory_abs_path.as_ref()
262                    == repo_path_owned.as_path()
263            })
264    });
265
266    if let Some(repo) = live_repo {
267        return Ok((repo, None));
268    }
269
270    let app_state =
271        current_app_state(cx).context("no app state available for temporary project")?;
272    let temp_project = cx.update(|cx| {
273        Project::local(
274            app_state.client.clone(),
275            app_state.node_runtime.clone(),
276            app_state.user_store.clone(),
277            app_state.languages.clone(),
278            app_state.fs.clone(),
279            None,
280            LocalProjectFlags::default(),
281            cx,
282        )
283    });
284
285    let repo_path_for_worktree = repo_path.to_path_buf();
286    let create_worktree = temp_project.update(cx, |project, cx| {
287        project.create_worktree(repo_path_for_worktree, true, cx)
288    });
289    let _worktree = create_worktree.await?;
290    let initial_scan = temp_project.read_with(cx, |project, cx| project.wait_for_initial_scan(cx));
291    initial_scan.await;
292
293    let repo_path_for_find = repo_path.to_path_buf();
294    let repo = temp_project
295        .update(cx, |project, cx| {
296            project
297                .repositories(cx)
298                .values()
299                .find(|repo| {
300                    repo.read(cx).snapshot().work_directory_abs_path.as_ref()
301                        == repo_path_for_find.as_path()
302                })
303                .cloned()
304        })
305        .context("failed to resolve temporary repository handle")?;
306
307    let barrier = repo.update(cx, |repo: &mut Repository, _cx| repo.barrier());
308    barrier
309        .await
310        .map_err(|_| anyhow!("temporary repository barrier canceled"))?;
311    Ok((repo, Some(temp_project)))
312}
313
314/// Re-adds the worktree to every affected project after a failed
315/// [`remove_root`].
316async fn rollback_root(root: &RootPlan, cx: &mut AsyncApp) {
317    for affected in &root.affected_projects {
318        let task = affected.project.update(cx, |project, cx| {
319            project.create_worktree(root.root_path.clone(), true, cx)
320        });
321        task.await.log_err();
322    }
323}
324
325/// Saves the worktree's full git state so it can be restored later.
326///
327/// This creates two detached commits (via [`create_archive_checkpoint`] on
328/// the `GitRepository` trait) that capture the staged and unstaged state
329/// without moving any branch ref. The commits are:
330///   - "WIP staged": a tree matching the current index, parented on HEAD
331///   - "WIP unstaged": a tree with all files (including untracked),
332///     parented on the staged commit
333///
334/// After creating the commits, this function:
335///   1. Records the commit SHAs, branch name, and paths in a DB record.
336///   2. Links every thread referencing this worktree to that record.
337///   3. Creates a git ref on the main repo to prevent GC of the commits.
338///
339/// On success, returns the archived worktree DB row ID for rollback.
340pub async fn persist_worktree_state(root: &RootPlan, cx: &mut AsyncApp) -> Result<i64> {
341    let worktree_repo = root.worktree_repo.clone();
342
343    let original_commit_hash = worktree_repo
344        .update(cx, |repo, _cx| repo.head_sha())
345        .await
346        .map_err(|_| anyhow!("head_sha canceled"))?
347        .context("failed to read original HEAD SHA")?
348        .context("HEAD SHA is None")?;
349
350    // Create two detached WIP commits without moving the branch.
351    let checkpoint_rx = worktree_repo.update(cx, |repo, _cx| repo.create_archive_checkpoint());
352    let (staged_commit_hash, unstaged_commit_hash) = checkpoint_rx
353        .await
354        .map_err(|_| anyhow!("create_archive_checkpoint canceled"))?
355        .context("failed to create archive checkpoint")?;
356
357    // Create DB record
358    let store = cx.update(|cx| ThreadMetadataStore::global(cx));
359    let worktree_path_str = root.root_path.to_string_lossy().to_string();
360    let main_repo_path_str = root.main_repo_path.to_string_lossy().to_string();
361    let branch_name = root.branch_name.clone().or_else(|| {
362        worktree_repo.read_with(cx, |repo, _cx| {
363            repo.snapshot()
364                .branch
365                .as_ref()
366                .map(|branch| branch.name().to_string())
367        })
368    });
369
370    let db_result = store
371        .read_with(cx, |store, cx| {
372            store.create_archived_worktree(
373                worktree_path_str.clone(),
374                main_repo_path_str.clone(),
375                branch_name.clone(),
376                staged_commit_hash.clone(),
377                unstaged_commit_hash.clone(),
378                original_commit_hash.clone(),
379                cx,
380            )
381        })
382        .await
383        .context("failed to create archived worktree DB record");
384    let archived_worktree_id = match db_result {
385        Ok(id) => id,
386        Err(error) => {
387            return Err(error);
388        }
389    };
390
391    // Link all threads on this worktree to the archived record
392    let session_ids: Vec<acp::SessionId> = store.read_with(cx, |store, _cx| {
393        store
394            .entries()
395            .filter(|thread| {
396                thread
397                    .folder_paths
398                    .paths()
399                    .iter()
400                    .any(|p| p.as_path() == root.root_path)
401            })
402            .map(|thread| thread.session_id.clone())
403            .collect()
404    });
405
406    for session_id in &session_ids {
407        let link_result = store
408            .read_with(cx, |store, cx| {
409                store.link_thread_to_archived_worktree(
410                    session_id.0.to_string(),
411                    archived_worktree_id,
412                    cx,
413                )
414            })
415            .await;
416        if let Err(error) = link_result {
417            if let Err(delete_error) = store
418                .read_with(cx, |store, cx| {
419                    store.delete_archived_worktree(archived_worktree_id, cx)
420                })
421                .await
422            {
423                log::error!(
424                    "Failed to delete archived worktree DB record during link rollback: \
425                     {delete_error:#}"
426                );
427            }
428            return Err(error.context("failed to link thread to archived worktree"));
429        }
430    }
431
432    // Create git ref on main repo to prevent GC of the detached commits.
433    // This is fatal: without the ref, git gc will eventually collect the
434    // WIP commits and a later restore will silently fail.
435    let ref_name = archived_worktree_ref_name(archived_worktree_id);
436    let (main_repo, _temp_project) = find_or_create_repository(&root.main_repo_path, cx)
437        .await
438        .context("could not open main repo to create archive ref")?;
439    let rx = main_repo.update(cx, |repo, _cx| {
440        repo.update_ref(ref_name.clone(), unstaged_commit_hash.clone())
441    });
442    rx.await
443        .map_err(|_| anyhow!("update_ref canceled"))
444        .and_then(|r| r)
445        .with_context(|| format!("failed to create ref {ref_name} on main repo"))?;
446    drop(_temp_project);
447
448    Ok(archived_worktree_id)
449}
450
451/// Undoes a successful [`persist_worktree_state`] by deleting the git ref
452/// on the main repo and removing the DB record. Since the WIP commits are
453/// detached (they don't move any branch), no git reset is needed — the
454/// commits will be garbage-collected once the ref is removed.
455pub async fn rollback_persist(archived_worktree_id: i64, root: &RootPlan, cx: &mut AsyncApp) {
456    // Delete the git ref on main repo
457    if let Ok((main_repo, _temp_project)) =
458        find_or_create_repository(&root.main_repo_path, cx).await
459    {
460        let ref_name = archived_worktree_ref_name(archived_worktree_id);
461        let rx = main_repo.update(cx, |repo, _cx| repo.delete_ref(ref_name));
462        rx.await.ok().and_then(|r| r.log_err());
463        drop(_temp_project);
464    }
465
466    // Delete the DB record
467    let store = cx.update(|cx| ThreadMetadataStore::global(cx));
468    if let Err(error) = store
469        .read_with(cx, |store, cx| {
470            store.delete_archived_worktree(archived_worktree_id, cx)
471        })
472        .await
473    {
474        log::error!("Failed to delete archived worktree DB record during rollback: {error:#}");
475    }
476}
477
478/// Restores a previously archived worktree back to disk from its DB record.
479///
480/// Creates the git worktree at the original commit (the branch never moved
481/// during archival since WIP commits are detached), switches to the branch,
482/// then uses [`restore_archive_checkpoint`] to reconstruct the staged/
483/// unstaged state from the WIP commit trees.
484pub async fn restore_worktree_via_git(
485    row: &ArchivedGitWorktree,
486    cx: &mut AsyncApp,
487) -> Result<PathBuf> {
488    let (main_repo, _temp_project) = find_or_create_repository(&row.main_repo_path, cx).await?;
489
490    let worktree_path = &row.worktree_path;
491    let app_state = current_app_state(cx).context("no app state available")?;
492    let already_exists = app_state.fs.metadata(worktree_path).await?.is_some();
493
494    let created_new_worktree = if already_exists {
495        let is_git_worktree =
496            resolve_git_worktree_to_main_repo(app_state.fs.as_ref(), worktree_path)
497                .await
498                .is_some();
499
500        if !is_git_worktree {
501            let rx = main_repo.update(cx, |repo, _cx| repo.repair_worktrees());
502            rx.await
503                .map_err(|_| anyhow!("worktree repair was canceled"))?
504                .context("failed to repair worktrees")?;
505        }
506        false
507    } else {
508        // Create worktree at the original commit — the branch still points
509        // here because archival used detached commits.
510        let rx = main_repo.update(cx, |repo, _cx| {
511            repo.create_worktree_detached(worktree_path.clone(), row.original_commit_hash.clone())
512        });
513        rx.await
514            .map_err(|_| anyhow!("worktree creation was canceled"))?
515            .context("failed to create worktree")?;
516        true
517    };
518
519    let (wt_repo, _temp_wt_project) = match find_or_create_repository(worktree_path, cx).await {
520        Ok(result) => result,
521        Err(error) => {
522            remove_new_worktree_on_error(created_new_worktree, &main_repo, worktree_path, cx).await;
523            return Err(error);
524        }
525    };
526
527    // Switch to the branch. Since the branch was never moved during
528    // archival (WIP commits are detached), it still points at
529    // original_commit_hash, so this is essentially a no-op for HEAD.
530    if let Some(branch_name) = &row.branch_name {
531        let rx = wt_repo.update(cx, |repo, _cx| repo.change_branch(branch_name.clone()));
532        if let Err(checkout_error) = rx.await.map_err(|e| anyhow!("{e}")).and_then(|r| r) {
533            log::debug!(
534                "change_branch('{}') failed: {checkout_error:#}, trying create_branch",
535                branch_name
536            );
537            let rx = wt_repo.update(cx, |repo, _cx| {
538                repo.create_branch(branch_name.clone(), None)
539            });
540            if let Ok(Err(error)) | Err(error) = rx.await.map_err(|e| anyhow!("{e}")) {
541                log::warn!(
542                    "Could not create branch '{}': {error} — \
543                     restored worktree will be in detached HEAD state.",
544                    branch_name
545                );
546            }
547        }
548    }
549
550    // Restore the staged/unstaged state from the WIP commit trees.
551    // read-tree --reset -u applies the unstaged tree (including deletions)
552    // to the working directory, then a bare read-tree sets the index to
553    // the staged tree without touching the working directory.
554    let restore_rx = wt_repo.update(cx, |repo, _cx| {
555        repo.restore_archive_checkpoint(
556            row.staged_commit_hash.clone(),
557            row.unstaged_commit_hash.clone(),
558        )
559    });
560    if let Err(error) = restore_rx
561        .await
562        .map_err(|_| anyhow!("restore_archive_checkpoint canceled"))
563        .and_then(|r| r)
564    {
565        remove_new_worktree_on_error(created_new_worktree, &main_repo, worktree_path, cx).await;
566        return Err(error.context("failed to restore archive checkpoint"));
567    }
568
569    Ok(worktree_path.clone())
570}
571
572async fn remove_new_worktree_on_error(
573    created_new_worktree: bool,
574    main_repo: &Entity<Repository>,
575    worktree_path: &PathBuf,
576    cx: &mut AsyncApp,
577) {
578    if created_new_worktree {
579        let rx = main_repo.update(cx, |repo, _cx| {
580            repo.remove_worktree(worktree_path.clone(), true)
581        });
582        rx.await.ok().and_then(|r| r.log_err());
583    }
584}
585
586/// Deletes the git ref and DB records for a single archived worktree.
587/// Used when an archived worktree is no longer referenced by any thread.
588pub async fn cleanup_archived_worktree_record(row: &ArchivedGitWorktree, cx: &mut AsyncApp) {
589    // Delete the git ref from the main repo
590    if let Ok((main_repo, _temp_project)) = find_or_create_repository(&row.main_repo_path, cx).await
591    {
592        let ref_name = archived_worktree_ref_name(row.id);
593        let rx = main_repo.update(cx, |repo, _cx| repo.delete_ref(ref_name));
594        match rx.await {
595            Ok(Ok(())) => {}
596            Ok(Err(error)) => log::warn!("Failed to delete archive ref: {error}"),
597            Err(_) => log::warn!("Archive ref deletion was canceled"),
598        }
599        // Keep _temp_project alive until after the await so the headless project isn't dropped mid-operation
600        drop(_temp_project);
601    }
602
603    // Delete the DB records
604    let store = cx.update(|cx| ThreadMetadataStore::global(cx));
605    store
606        .read_with(cx, |store, cx| store.delete_archived_worktree(row.id, cx))
607        .await
608        .log_err();
609}
610
611/// Cleans up all archived worktree data associated with a thread being deleted.
612///
613/// This unlinks the thread from all its archived worktrees and, for any
614/// archived worktree that is no longer referenced by any other thread,
615/// deletes the git ref and DB records.
616pub async fn cleanup_thread_archived_worktrees(session_id: &acp::SessionId, cx: &mut AsyncApp) {
617    let store = cx.update(|cx| ThreadMetadataStore::global(cx));
618
619    let archived_worktrees = store
620        .read_with(cx, |store, cx| {
621            store.get_archived_worktrees_for_thread(session_id.0.to_string(), cx)
622        })
623        .await;
624    let archived_worktrees = match archived_worktrees {
625        Ok(rows) => rows,
626        Err(error) => {
627            log::error!(
628                "Failed to fetch archived worktrees for thread {}: {error:#}",
629                session_id.0
630            );
631            return;
632        }
633    };
634
635    if archived_worktrees.is_empty() {
636        return;
637    }
638
639    if let Err(error) = store
640        .read_with(cx, |store, cx| {
641            store.unlink_thread_from_all_archived_worktrees(session_id.0.to_string(), cx)
642        })
643        .await
644    {
645        log::error!(
646            "Failed to unlink thread {} from archived worktrees: {error:#}",
647            session_id.0
648        );
649        return;
650    }
651
652    for row in &archived_worktrees {
653        let still_referenced = store
654            .read_with(cx, |store, cx| {
655                store.is_archived_worktree_referenced(row.id, cx)
656            })
657            .await;
658        match still_referenced {
659            Ok(true) => {}
660            Ok(false) => {
661                cleanup_archived_worktree_record(row, cx).await;
662            }
663            Err(error) => {
664                log::error!(
665                    "Failed to check if archived worktree {} is still referenced: {error:#}",
666                    row.id
667                );
668            }
669        }
670    }
671}
672
673/// Collects every `Workspace` entity across all open `MultiWorkspace` windows.
674pub fn all_open_workspaces(cx: &App) -> Vec<Entity<Workspace>> {
675    cx.windows()
676        .into_iter()
677        .filter_map(|window| window.downcast::<MultiWorkspace>())
678        .flat_map(|multi_workspace| {
679            multi_workspace
680                .read(cx)
681                .map(|multi_workspace| multi_workspace.workspaces().cloned().collect::<Vec<_>>())
682                .unwrap_or_default()
683        })
684        .collect()
685}
686
687fn current_app_state(cx: &mut AsyncApp) -> Option<Arc<AppState>> {
688    cx.update(|cx| {
689        all_open_workspaces(cx)
690            .into_iter()
691            .next()
692            .map(|workspace| workspace.read(cx).app_state().clone())
693    })
694}