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 git::repository::{AskPassDelegate, CommitOptions, ResetMode};
  9use gpui::{App, AsyncApp, Entity, Task, WindowHandle};
 10use project::{
 11    LocalProjectFlags, Project, WorktreeId,
 12    git_store::{Repository, resolve_git_worktree_to_main_repo},
 13};
 14use util::ResultExt;
 15use workspace::{AppState, MultiWorkspace, PathList, Workspace};
 16
 17use crate::thread_metadata_store::{ArchivedGitWorktree, ThreadMetadataStore};
 18
 19#[derive(Clone)]
 20pub struct RootPlan {
 21    pub root_path: PathBuf,
 22    pub main_repo_path: PathBuf,
 23    pub affected_projects: Vec<AffectedProject>,
 24    pub worktree_repo: Option<Entity<Repository>>,
 25    pub branch_name: Option<String>,
 26}
 27
 28#[derive(Clone)]
 29pub struct AffectedProject {
 30    pub project: Entity<Project>,
 31    pub worktree_id: WorktreeId,
 32}
 33
 34fn archived_worktree_ref_name(id: i64) -> String {
 35    format!("refs/archived-worktrees/{}", id)
 36}
 37
 38pub struct PersistOutcome {
 39    pub archived_worktree_id: i64,
 40    pub staged_commit_hash: String,
 41}
 42
 43pub fn build_root_plan(
 44    path: &Path,
 45    workspaces: &[Entity<Workspace>],
 46    cx: &App,
 47) -> Option<RootPlan> {
 48    let path = path.to_path_buf();
 49
 50    let affected_projects = workspaces
 51        .iter()
 52        .filter_map(|workspace| {
 53            let project = workspace.read(cx).project().clone();
 54            let worktree = project
 55                .read(cx)
 56                .visible_worktrees(cx)
 57                .find(|worktree| worktree.read(cx).abs_path().as_ref() == path.as_path())?;
 58            let worktree_id = worktree.read(cx).id();
 59            Some(AffectedProject {
 60                project,
 61                worktree_id,
 62            })
 63        })
 64        .collect::<Vec<_>>();
 65
 66    let (linked_snapshot, worktree_repo) = workspaces
 67        .iter()
 68        .flat_map(|workspace| {
 69            workspace
 70                .read(cx)
 71                .project()
 72                .read(cx)
 73                .repositories(cx)
 74                .values()
 75                .cloned()
 76                .collect::<Vec<_>>()
 77        })
 78        .find_map(|repo| {
 79            let snapshot = repo.read(cx).snapshot();
 80            (snapshot.is_linked_worktree()
 81                && snapshot.work_directory_abs_path.as_ref() == path.as_path())
 82            .then_some((snapshot, repo))
 83        })?;
 84
 85    let branch_name = linked_snapshot
 86        .branch
 87        .as_ref()
 88        .map(|b| b.name().to_string());
 89
 90    Some(RootPlan {
 91        root_path: path,
 92        main_repo_path: linked_snapshot.original_repo_abs_path.to_path_buf(),
 93        affected_projects,
 94        worktree_repo: Some(worktree_repo),
 95        branch_name,
 96    })
 97}
 98
 99pub fn path_is_referenced_by_other_unarchived_threads(
100    current_session_id: &acp::SessionId,
101    path: &Path,
102    cx: &App,
103) -> bool {
104    ThreadMetadataStore::global(cx)
105        .read(cx)
106        .entries()
107        .filter(|thread| thread.session_id != *current_session_id)
108        .filter(|thread| !thread.archived)
109        .any(|thread| {
110            thread
111                .folder_paths
112                .paths()
113                .iter()
114                .any(|other_path| other_path.as_path() == path)
115        })
116}
117
118pub async fn remove_root(root: RootPlan, cx: &mut AsyncApp) -> Result<()> {
119    let release_tasks: Vec<_> = root
120        .affected_projects
121        .iter()
122        .map(|affected| {
123            let project = affected.project.clone();
124            let worktree_id = affected.worktree_id;
125            project.update(cx, |project, cx| {
126                let wait = project.wait_for_worktree_release(worktree_id, cx);
127                project.remove_worktree(worktree_id, cx);
128                wait
129            })
130        })
131        .collect();
132
133    if let Err(error) = remove_root_after_worktree_removal(&root, release_tasks, cx).await {
134        rollback_root(&root, cx).await;
135        return Err(error);
136    }
137
138    Ok(())
139}
140
141async fn remove_root_after_worktree_removal(
142    root: &RootPlan,
143    release_tasks: Vec<Task<Result<()>>>,
144    cx: &mut AsyncApp,
145) -> Result<()> {
146    for task in release_tasks {
147        task.await?;
148    }
149
150    let (repo, _temp_project) = find_or_create_repository(&root.main_repo_path, cx).await?;
151    let receiver = repo.update(cx, |repo: &mut Repository, _cx| {
152        repo.remove_worktree(root.root_path.clone(), false)
153    });
154    let result = receiver
155        .await
156        .map_err(|_| anyhow!("git worktree removal was canceled"))?;
157    result
158}
159
160/// Finds a live `Repository` entity for the given path, or creates a temporary
161/// `Project::local` to obtain one.
162///
163/// `Repository` entities can only be obtained through a `Project` because
164/// `GitStore` (which creates and manages `Repository` entities) is owned by
165/// `Project`. When no open workspace contains the repo we need, we spin up a
166/// headless `Project::local` just to get a `Repository` handle. The caller
167/// keeps the returned `Option<Entity<Project>>` alive for the duration of the
168/// git operations, then drops it.
169///
170/// Future improvement: decoupling `GitStore` from `Project` so that
171/// `Repository` entities can be created standalone would eliminate this
172/// temporary-project workaround.
173async fn find_or_create_repository(
174    repo_path: &Path,
175    cx: &mut AsyncApp,
176) -> Result<(Entity<Repository>, Option<Entity<Project>>)> {
177    let repo_path_owned = repo_path.to_path_buf();
178    let live_repo = cx.update(|cx| {
179        all_open_workspaces(cx)
180            .into_iter()
181            .flat_map(|workspace| {
182                workspace
183                    .read(cx)
184                    .project()
185                    .read(cx)
186                    .repositories(cx)
187                    .values()
188                    .cloned()
189                    .collect::<Vec<_>>()
190            })
191            .find(|repo| {
192                repo.read(cx).snapshot().work_directory_abs_path.as_ref()
193                    == repo_path_owned.as_path()
194            })
195    });
196
197    if let Some(repo) = live_repo {
198        return Ok((repo, None));
199    }
200
201    let app_state =
202        current_app_state(cx).context("no app state available for temporary project")?;
203    let temp_project = cx.update(|cx| {
204        Project::local(
205            app_state.client.clone(),
206            app_state.node_runtime.clone(),
207            app_state.user_store.clone(),
208            app_state.languages.clone(),
209            app_state.fs.clone(),
210            None,
211            LocalProjectFlags::default(),
212            cx,
213        )
214    });
215
216    let repo_path_for_worktree = repo_path.to_path_buf();
217    let create_worktree = temp_project.update(cx, |project, cx| {
218        project.create_worktree(repo_path_for_worktree, true, cx)
219    });
220    let _worktree = create_worktree.await?;
221    let initial_scan = temp_project.read_with(cx, |project, cx| project.wait_for_initial_scan(cx));
222    initial_scan.await;
223
224    let repo_path_for_find = repo_path.to_path_buf();
225    let repo = temp_project
226        .update(cx, |project, cx| {
227            project
228                .repositories(cx)
229                .values()
230                .find(|repo| {
231                    repo.read(cx).snapshot().work_directory_abs_path.as_ref()
232                        == repo_path_for_find.as_path()
233                })
234                .cloned()
235        })
236        .context("failed to resolve temporary repository handle")?;
237
238    let barrier = repo.update(cx, |repo: &mut Repository, _cx| repo.barrier());
239    barrier
240        .await
241        .map_err(|_| anyhow!("temporary repository barrier canceled"))?;
242    Ok((repo, Some(temp_project)))
243}
244
245async fn rollback_root(root: &RootPlan, cx: &mut AsyncApp) {
246    for affected in &root.affected_projects {
247        let task = affected.project.update(cx, |project, cx| {
248            project.create_worktree(root.root_path.clone(), true, cx)
249        });
250        let _ = task.await;
251    }
252}
253
254pub async fn persist_worktree_state(
255    root: &RootPlan,
256    folder_paths: &PathList,
257    cx: &mut AsyncApp,
258) -> Result<PersistOutcome> {
259    let worktree_repo = root
260        .worktree_repo
261        .clone()
262        .context("no worktree repo entity for persistence")?;
263
264    // Read original HEAD SHA before creating any WIP commits
265    let original_commit_hash = worktree_repo
266        .update(cx, |repo, _cx| repo.head_sha())
267        .await
268        .map_err(|_| anyhow!("head_sha canceled"))?
269        .context("failed to read original HEAD SHA")?
270        .context("HEAD SHA is None before WIP commits")?;
271
272    // Create WIP commit #1 (staged state)
273    let askpass = AskPassDelegate::new(cx, |_, _, _| {});
274    let commit_rx = worktree_repo.update(cx, |repo, cx| {
275        repo.commit(
276            "WIP staged".into(),
277            None,
278            CommitOptions {
279                allow_empty: true,
280                ..Default::default()
281            },
282            askpass,
283            cx,
284        )
285    });
286    commit_rx
287        .await
288        .map_err(|_| anyhow!("WIP staged commit canceled"))??;
289
290    // Read SHA after staged commit
291    let staged_sha_result = worktree_repo
292        .update(cx, |repo, _cx| repo.head_sha())
293        .await
294        .map_err(|_| anyhow!("head_sha canceled"))
295        .and_then(|r| r.context("failed to read HEAD SHA after staged commit"))
296        .and_then(|opt| opt.context("HEAD SHA is None after staged commit"));
297    let staged_commit_hash = match staged_sha_result {
298        Ok(sha) => sha,
299        Err(error) => {
300            let rx = worktree_repo.update(cx, |repo, cx| {
301                repo.reset("HEAD~1".to_string(), ResetMode::Mixed, cx)
302            });
303            let _ = rx.await;
304            return Err(error);
305        }
306    };
307
308    // Stage all files including untracked
309    let stage_rx = worktree_repo.update(cx, |repo, _cx| repo.stage_all_including_untracked());
310    if let Err(error) = stage_rx
311        .await
312        .map_err(|_| anyhow!("stage all canceled"))
313        .and_then(|inner| inner)
314    {
315        let rx = worktree_repo.update(cx, |repo, cx| {
316            repo.reset("HEAD~1".to_string(), ResetMode::Mixed, cx)
317        });
318        let _ = rx.await;
319        return Err(error.context("failed to stage all files including untracked"));
320    }
321
322    // Create WIP commit #2 (unstaged/untracked state)
323    let askpass = AskPassDelegate::new(cx, |_, _, _| {});
324    let commit_rx = worktree_repo.update(cx, |repo, cx| {
325        repo.commit(
326            "WIP unstaged".into(),
327            None,
328            CommitOptions {
329                allow_empty: true,
330                ..Default::default()
331            },
332            askpass,
333            cx,
334        )
335    });
336    if let Err(error) = commit_rx
337        .await
338        .map_err(|_| anyhow!("WIP unstaged commit canceled"))
339        .and_then(|inner| inner)
340    {
341        let rx = worktree_repo.update(cx, |repo, cx| {
342            repo.reset("HEAD~1".to_string(), ResetMode::Mixed, cx)
343        });
344        let _ = rx.await;
345        return Err(error);
346    }
347
348    // Read HEAD SHA after WIP commits
349    let head_sha_result = worktree_repo
350        .update(cx, |repo, _cx| repo.head_sha())
351        .await
352        .map_err(|_| anyhow!("head_sha canceled"))
353        .and_then(|r| r.context("failed to read HEAD SHA after WIP commits"))
354        .and_then(|opt| opt.context("HEAD SHA is None after WIP commits"));
355    let unstaged_commit_hash = match head_sha_result {
356        Ok(sha) => sha,
357        Err(error) => {
358            let rx = worktree_repo.update(cx, |repo, cx| {
359                repo.reset(format!("{}~1", staged_commit_hash), ResetMode::Mixed, cx)
360            });
361            let _ = rx.await;
362            return Err(error);
363        }
364    };
365
366    // Create DB record
367    let store = cx.update(|cx| ThreadMetadataStore::global(cx));
368    let worktree_path_str = root.root_path.to_string_lossy().to_string();
369    let main_repo_path_str = root.main_repo_path.to_string_lossy().to_string();
370    let branch_name = root.branch_name.clone();
371
372    let db_result = store
373        .read_with(cx, |store, cx| {
374            store.create_archived_worktree(
375                worktree_path_str.clone(),
376                main_repo_path_str.clone(),
377                branch_name.clone(),
378                staged_commit_hash.clone(),
379                unstaged_commit_hash.clone(),
380                original_commit_hash.clone(),
381                cx,
382            )
383        })
384        .await
385        .context("failed to create archived worktree DB record");
386    let archived_worktree_id = match db_result {
387        Ok(id) => id,
388        Err(error) => {
389            let rx = worktree_repo.update(cx, |repo, cx| {
390                repo.reset(format!("{}~1", staged_commit_hash), ResetMode::Mixed, cx)
391            });
392            let _ = rx.await;
393            return Err(error);
394        }
395    };
396
397    // Link all threads on this worktree to the archived record
398    let session_ids: Vec<acp::SessionId> = store.read_with(cx, |store, _cx| {
399        store
400            .all_session_ids_for_path(folder_paths)
401            .cloned()
402            .collect()
403    });
404
405    for session_id in &session_ids {
406        let link_result = store
407            .read_with(cx, |store, cx| {
408                store.link_thread_to_archived_worktree(
409                    session_id.0.to_string(),
410                    archived_worktree_id,
411                    cx,
412                )
413            })
414            .await;
415        if let Err(error) = link_result {
416            if let Err(delete_error) = store
417                .read_with(cx, |store, cx| {
418                    store.delete_archived_worktree(archived_worktree_id, cx)
419                })
420                .await
421            {
422                log::error!(
423                    "Failed to delete archived worktree DB record during link rollback: {delete_error:#}"
424                );
425            }
426            let rx = worktree_repo.update(cx, |repo, cx| {
427                repo.reset(format!("{}~1", staged_commit_hash), ResetMode::Mixed, cx)
428            });
429            let _ = rx.await;
430            return Err(error.context("failed to link thread to archived worktree"));
431        }
432    }
433
434    // Create git ref on main repo (non-fatal)
435    let ref_name = archived_worktree_ref_name(archived_worktree_id);
436    let main_repo_result = find_or_create_repository(&root.main_repo_path, cx).await;
437    match main_repo_result {
438        Ok((main_repo, _temp_project)) => {
439            let rx = main_repo.update(cx, |repo, _cx| {
440                repo.update_ref(ref_name.clone(), unstaged_commit_hash.clone())
441            });
442            if let Err(error) = rx
443                .await
444                .map_err(|_| anyhow!("update_ref canceled"))
445                .and_then(|r| r)
446            {
447                log::warn!(
448                    "Failed to create ref {} on main repo (non-fatal): {error}",
449                    ref_name
450                );
451            }
452        }
453        Err(error) => {
454            log::warn!(
455                "Could not find main repo to create ref {} (non-fatal): {error}",
456                ref_name
457            );
458        }
459    }
460
461    Ok(PersistOutcome {
462        archived_worktree_id,
463        staged_commit_hash,
464    })
465}
466
467pub async fn rollback_persist(outcome: &PersistOutcome, root: &RootPlan, cx: &mut AsyncApp) {
468    // Undo WIP commits on the worktree repo
469    if let Some(worktree_repo) = &root.worktree_repo {
470        let rx = worktree_repo.update(cx, |repo, cx| {
471            repo.reset(
472                format!("{}~1", outcome.staged_commit_hash),
473                ResetMode::Mixed,
474                cx,
475            )
476        });
477        let _ = rx.await;
478    }
479
480    // Delete the git ref on main repo
481    if let Ok((main_repo, _temp_project)) =
482        find_or_create_repository(&root.main_repo_path, cx).await
483    {
484        let ref_name = archived_worktree_ref_name(outcome.archived_worktree_id);
485        let rx = main_repo.update(cx, |repo, _cx| repo.delete_ref(ref_name));
486        let _ = rx.await;
487    }
488
489    // Delete the DB record
490    let store = cx.update(|cx| ThreadMetadataStore::global(cx));
491    if let Err(error) = store
492        .read_with(cx, |store, cx| {
493            store.delete_archived_worktree(outcome.archived_worktree_id, cx)
494        })
495        .await
496    {
497        log::error!("Failed to delete archived worktree DB record during rollback: {error:#}");
498    }
499}
500
501pub async fn restore_worktree_via_git(
502    row: &ArchivedGitWorktree,
503    cx: &mut AsyncApp,
504) -> Result<PathBuf> {
505    // Find the main repo entity and verify original_commit_hash exists
506    let (main_repo, _temp_project) = find_or_create_repository(&row.main_repo_path, cx).await?;
507
508    let commit_exists = main_repo
509        .update(cx, |repo, _cx| {
510            repo.resolve_commit(row.original_commit_hash.clone())
511        })
512        .await
513        .map_err(|_| anyhow!("resolve_commit was canceled"))?
514        .context("failed to check if original commit exists")?;
515
516    if !commit_exists {
517        anyhow::bail!(
518            "Original commit {} no longer exists in the repository — \
519             cannot restore worktree. The git history this archive depends on may have been \
520             rewritten or garbage-collected.",
521            row.original_commit_hash
522        );
523    }
524
525    // Check if worktree path already exists on disk
526    let worktree_path = &row.worktree_path;
527    let app_state = current_app_state(cx).context("no app state available")?;
528    let already_exists = app_state.fs.metadata(worktree_path).await?.is_some();
529
530    if already_exists {
531        let is_git_worktree =
532            resolve_git_worktree_to_main_repo(app_state.fs.as_ref(), worktree_path)
533                .await
534                .is_some();
535
536        if is_git_worktree {
537            // Already a git worktree — another thread on the same worktree
538            // already restored it. Reuse as-is.
539            return Ok(worktree_path.clone());
540        }
541
542        // Path exists but isn't a git worktree. Ask git to adopt it.
543        let rx = main_repo.update(cx, |repo, _cx| repo.repair_worktrees());
544        rx.await
545            .map_err(|_| anyhow!("worktree repair was canceled"))?
546            .context("failed to repair worktrees")?;
547    } else {
548        // Create detached worktree at the unstaged commit
549        let rx = main_repo.update(cx, |repo, _cx| {
550            repo.create_worktree_detached(worktree_path.clone(), row.unstaged_commit_hash.clone())
551        });
552        rx.await
553            .map_err(|_| anyhow!("worktree creation was canceled"))?
554            .context("failed to create worktree")?;
555    }
556
557    // Get the worktree's repo entity
558    let (wt_repo, _temp_wt_project) = find_or_create_repository(worktree_path, cx).await?;
559
560    // Reset past the WIP commits to recover original state
561    let mixed_reset_ok = {
562        let rx = wt_repo.update(cx, |repo, cx| {
563            repo.reset(row.staged_commit_hash.clone(), ResetMode::Mixed, cx)
564        });
565        match rx.await {
566            Ok(Ok(())) => true,
567            Ok(Err(error)) => {
568                log::error!("Mixed reset to staged commit failed: {error:#}");
569                false
570            }
571            Err(_) => {
572                log::error!("Mixed reset to staged commit was canceled");
573                false
574            }
575        }
576    };
577
578    let soft_reset_ok = if mixed_reset_ok {
579        let rx = wt_repo.update(cx, |repo, cx| {
580            repo.reset(row.original_commit_hash.clone(), ResetMode::Soft, cx)
581        });
582        match rx.await {
583            Ok(Ok(())) => true,
584            Ok(Err(error)) => {
585                log::error!("Soft reset to original commit failed: {error:#}");
586                false
587            }
588            Err(_) => {
589                log::error!("Soft reset to original commit was canceled");
590                false
591            }
592        }
593    } else {
594        false
595    };
596
597    // If either WIP reset failed, fall back to a mixed reset directly to
598    // original_commit_hash so we at least land on the right commit.
599    if !mixed_reset_ok || !soft_reset_ok {
600        log::warn!(
601            "WIP reset(s) failed (mixed_ok={mixed_reset_ok}, soft_ok={soft_reset_ok}); \
602             falling back to mixed reset to original commit {}",
603            row.original_commit_hash
604        );
605        let rx = wt_repo.update(cx, |repo, cx| {
606            repo.reset(row.original_commit_hash.clone(), ResetMode::Mixed, cx)
607        });
608        match rx.await {
609            Ok(Ok(())) => {}
610            Ok(Err(error)) => {
611                return Err(error.context(format!(
612                    "fallback reset to original commit {} also failed",
613                    row.original_commit_hash
614                )));
615            }
616            Err(_) => {
617                return Err(anyhow!(
618                    "fallback reset to original commit {} was canceled",
619                    row.original_commit_hash
620                ));
621            }
622        }
623    }
624
625    // Verify HEAD is at original_commit_hash
626    let current_head = wt_repo
627        .update(cx, |repo, _cx| repo.head_sha())
628        .await
629        .map_err(|_| anyhow!("post-restore head_sha was canceled"))?
630        .context("failed to read HEAD after restore")?
631        .context("HEAD is None after restore")?;
632
633    if current_head != row.original_commit_hash {
634        anyhow::bail!(
635            "After restore, HEAD is at {current_head} but expected {}. \
636             The worktree may be in an inconsistent state.",
637            row.original_commit_hash
638        );
639    }
640
641    // Restore the branch
642    if let Some(branch_name) = &row.branch_name {
643        // Check if the branch exists and points at original_commit_hash.
644        // If it does, switch to it. If not, create a new branch there.
645        let rx = wt_repo.update(cx, |repo, _cx| repo.change_branch(branch_name.clone()));
646        if matches!(rx.await, Ok(Ok(()))) {
647            // Verify the branch actually points at original_commit_hash after switching
648            let head_after_switch = wt_repo
649                .update(cx, |repo, _cx| repo.head_sha())
650                .await
651                .ok()
652                .and_then(|r| r.ok())
653                .flatten();
654
655            if head_after_switch.as_deref() != Some(&row.original_commit_hash) {
656                // Branch exists but doesn't point at the right commit.
657                // Switch back to detached HEAD at original_commit_hash.
658                log::warn!(
659                    "Branch '{}' exists but points at {:?}, not {}. Creating fresh branch.",
660                    branch_name,
661                    head_after_switch,
662                    row.original_commit_hash
663                );
664                let rx = wt_repo.update(cx, |repo, cx| {
665                    repo.reset(row.original_commit_hash.clone(), ResetMode::Mixed, cx)
666                });
667                let _ = rx.await;
668                // Delete the old branch and create fresh
669                let rx = wt_repo.update(cx, |repo, _cx| {
670                    repo.create_branch(branch_name.clone(), None)
671                });
672                let _ = rx.await;
673            }
674        } else {
675            // Branch doesn't exist or can't be switched to — create it.
676            let rx = wt_repo.update(cx, |repo, _cx| {
677                repo.create_branch(branch_name.clone(), None)
678            });
679            if let Ok(Err(error)) | Err(error) = rx.await.map_err(|e| anyhow::anyhow!("{e}")) {
680                log::warn!(
681                    "Could not create branch '{}': {error} — \
682                     restored worktree is in detached HEAD state.",
683                    branch_name
684                );
685            }
686        }
687    }
688
689    Ok(worktree_path.clone())
690}
691
692pub async fn cleanup_archived_worktree_record(row: &ArchivedGitWorktree, cx: &mut AsyncApp) {
693    // Delete the git ref from the main repo
694    if let Ok((main_repo, _temp_project)) = find_or_create_repository(&row.main_repo_path, cx).await
695    {
696        let ref_name = archived_worktree_ref_name(row.id);
697        let rx = main_repo.update(cx, |repo, _cx| repo.delete_ref(ref_name));
698        match rx.await {
699            Ok(Ok(())) => {}
700            Ok(Err(error)) => log::warn!("Failed to delete archive ref: {error}"),
701            Err(_) => log::warn!("Archive ref deletion was canceled"),
702        }
703    }
704
705    // Delete the DB records
706    let store = cx.update(|cx| ThreadMetadataStore::global(cx));
707    store
708        .read_with(cx, |store, cx| store.delete_archived_worktree(row.id, cx))
709        .await
710        .log_err();
711}
712
713pub fn all_open_workspaces(cx: &App) -> Vec<Entity<Workspace>> {
714    cx.windows()
715        .into_iter()
716        .filter_map(|window| window.downcast::<MultiWorkspace>())
717        .flat_map(|multi_workspace| {
718            multi_workspace
719                .read(cx)
720                .map(|multi_workspace| multi_workspace.workspaces().to_vec())
721                .unwrap_or_default()
722        })
723        .collect()
724}
725
726fn window_for_workspace(
727    workspace: &Entity<Workspace>,
728    cx: &App,
729) -> Option<WindowHandle<MultiWorkspace>> {
730    cx.windows()
731        .into_iter()
732        .filter_map(|window| window.downcast::<MultiWorkspace>())
733        .find(|window| {
734            window
735                .read(cx)
736                .map(|multi_workspace| multi_workspace.workspaces().contains(workspace))
737                .unwrap_or(false)
738        })
739}
740
741fn window_for_workspace_async(
742    workspace: &Entity<Workspace>,
743    cx: &mut AsyncApp,
744) -> Option<WindowHandle<MultiWorkspace>> {
745    let workspace = workspace.clone();
746    cx.update(|cx| window_for_workspace(&workspace, cx))
747}
748
749fn current_app_state(cx: &mut AsyncApp) -> Option<Arc<AppState>> {
750    cx.update(|cx| {
751        all_open_workspaces(cx)
752            .into_iter()
753            .next()
754            .map(|workspace| workspace.read(cx).app_state().clone())
755    })
756}