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}