recent_projects.rs

   1mod dev_container_suggest;
   2pub mod disconnected_overlay;
   3mod remote_connections;
   4mod remote_servers;
   5mod ssh_config;
   6
   7use std::{
   8    collections::HashSet,
   9    path::{Path, PathBuf},
  10    sync::Arc,
  11};
  12
  13use chrono::{DateTime, Utc};
  14
  15use fs::Fs;
  16
  17#[cfg(target_os = "windows")]
  18mod wsl_picker;
  19
  20use remote::RemoteConnectionOptions;
  21pub use remote_connection::{RemoteConnectionModal, connect};
  22pub use remote_connections::{navigate_to_positions, open_remote_project};
  23
  24use disconnected_overlay::DisconnectedOverlay;
  25use fuzzy::{StringMatch, StringMatchCandidate};
  26use gpui::{
  27    Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
  28    Subscription, Task, WeakEntity, Window, actions, px,
  29};
  30
  31use picker::{
  32    Picker, PickerDelegate,
  33    highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths},
  34};
  35use project::{Worktree, git_store::Repository};
  36pub use remote_connections::RemoteSettings;
  37pub use remote_servers::RemoteServerProjects;
  38use settings::{Settings, WorktreeId};
  39use ui_input::ErasedEditor;
  40
  41use dev_container::{DevContainerContext, find_devcontainer_configs};
  42use ui::{
  43    ContextMenu, Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, PopoverMenu,
  44    PopoverMenuHandle, TintColor, Tooltip, prelude::*,
  45};
  46use util::{ResultExt, paths::PathExt};
  47use workspace::{
  48    HistoryManager, ModalView, MultiWorkspace, OpenOptions, OpenVisible, PathList,
  49    SerializedWorkspaceLocation, Workspace, WorkspaceDb, WorkspaceId,
  50    notifications::DetachAndPromptErr, with_active_or_new_workspace,
  51};
  52use zed_actions::{OpenDevContainer, OpenRecent, OpenRemote};
  53
  54actions!(recent_projects, [ToggleActionsMenu]);
  55
  56#[derive(Clone, Debug)]
  57pub struct RecentProjectEntry {
  58    pub name: SharedString,
  59    pub full_path: SharedString,
  60    pub paths: Vec<PathBuf>,
  61    pub workspace_id: WorkspaceId,
  62    pub timestamp: DateTime<Utc>,
  63}
  64
  65#[derive(Clone, Debug)]
  66struct OpenFolderEntry {
  67    worktree_id: WorktreeId,
  68    name: SharedString,
  69    path: PathBuf,
  70    branch: Option<SharedString>,
  71    is_active: bool,
  72}
  73
  74#[derive(Clone, Debug)]
  75enum ProjectPickerEntry {
  76    Header(SharedString),
  77    OpenFolder { index: usize, positions: Vec<usize> },
  78    OpenProject(StringMatch),
  79    RecentProject(StringMatch),
  80}
  81
  82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
  83enum ProjectPickerStyle {
  84    Modal,
  85    Popover,
  86}
  87
  88pub async fn get_recent_projects(
  89    current_workspace_id: Option<WorkspaceId>,
  90    limit: Option<usize>,
  91    fs: Arc<dyn fs::Fs>,
  92    db: &WorkspaceDb,
  93) -> Vec<RecentProjectEntry> {
  94    let workspaces = db
  95        .recent_workspaces_on_disk(fs.as_ref())
  96        .await
  97        .unwrap_or_default();
  98
  99    let entries: Vec<RecentProjectEntry> = workspaces
 100        .into_iter()
 101        .filter(|(id, _, _, _)| Some(*id) != current_workspace_id)
 102        .filter(|(_, location, _, _)| matches!(location, SerializedWorkspaceLocation::Local))
 103        .map(|(workspace_id, _, path_list, timestamp)| {
 104            let paths: Vec<PathBuf> = path_list.paths().to_vec();
 105            let ordered_paths: Vec<&PathBuf> = path_list.ordered_paths().collect();
 106
 107            let name = if ordered_paths.len() == 1 {
 108                ordered_paths[0]
 109                    .file_name()
 110                    .map(|n| n.to_string_lossy().to_string())
 111                    .unwrap_or_else(|| ordered_paths[0].to_string_lossy().to_string())
 112            } else {
 113                ordered_paths
 114                    .iter()
 115                    .filter_map(|p| p.file_name())
 116                    .map(|n| n.to_string_lossy().to_string())
 117                    .collect::<Vec<_>>()
 118                    .join(", ")
 119            };
 120
 121            let full_path = ordered_paths
 122                .iter()
 123                .map(|p| p.to_string_lossy().to_string())
 124                .collect::<Vec<_>>()
 125                .join("\n");
 126
 127            RecentProjectEntry {
 128                name: SharedString::from(name),
 129                full_path: SharedString::from(full_path),
 130                paths,
 131                workspace_id,
 132                timestamp,
 133            }
 134        })
 135        .collect();
 136
 137    match limit {
 138        Some(n) => entries.into_iter().take(n).collect(),
 139        None => entries,
 140    }
 141}
 142
 143pub async fn delete_recent_project(workspace_id: WorkspaceId, db: &WorkspaceDb) {
 144    let _ = db.delete_workspace_by_id(workspace_id).await;
 145}
 146
 147fn get_open_folders(workspace: &Workspace, cx: &App) -> Vec<OpenFolderEntry> {
 148    let project = workspace.project().read(cx);
 149    let visible_worktrees: Vec<_> = project.visible_worktrees(cx).collect();
 150
 151    if visible_worktrees.len() <= 1 {
 152        return Vec::new();
 153    }
 154
 155    let active_worktree_id = workspace.active_worktree_override().or_else(|| {
 156        if let Some(repo) = project.active_repository(cx) {
 157            let repo = repo.read(cx);
 158            let repo_path = &repo.work_directory_abs_path;
 159            for worktree in project.visible_worktrees(cx) {
 160                let worktree_path = worktree.read(cx).abs_path();
 161                if worktree_path == *repo_path || worktree_path.starts_with(repo_path.as_ref()) {
 162                    return Some(worktree.read(cx).id());
 163                }
 164            }
 165        }
 166        project
 167            .visible_worktrees(cx)
 168            .next()
 169            .map(|wt| wt.read(cx).id())
 170    });
 171
 172    let git_store = project.git_store().read(cx);
 173    let repositories: Vec<_> = git_store.repositories().values().cloned().collect();
 174
 175    let mut entries: Vec<OpenFolderEntry> = visible_worktrees
 176        .into_iter()
 177        .map(|worktree| {
 178            let worktree_ref = worktree.read(cx);
 179            let worktree_id = worktree_ref.id();
 180            let name = SharedString::from(worktree_ref.root_name().as_unix_str().to_string());
 181            let path = worktree_ref.abs_path().to_path_buf();
 182            let branch = get_branch_for_worktree(worktree_ref, &repositories, cx);
 183            let is_active = active_worktree_id == Some(worktree_id);
 184            OpenFolderEntry {
 185                worktree_id,
 186                name,
 187                path,
 188                branch,
 189                is_active,
 190            }
 191        })
 192        .collect();
 193
 194    entries.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
 195    entries
 196}
 197
 198fn get_branch_for_worktree(
 199    worktree: &Worktree,
 200    repositories: &[Entity<Repository>],
 201    cx: &App,
 202) -> Option<SharedString> {
 203    let worktree_abs_path = worktree.abs_path();
 204    for repo in repositories {
 205        let repo = repo.read(cx);
 206        if repo.work_directory_abs_path == worktree_abs_path
 207            || worktree_abs_path.starts_with(&*repo.work_directory_abs_path)
 208        {
 209            if let Some(branch) = &repo.branch {
 210                return Some(SharedString::from(branch.name().to_string()));
 211            }
 212        }
 213    }
 214    None
 215}
 216
 217pub fn init(cx: &mut App) {
 218    #[cfg(target_os = "windows")]
 219    cx.on_action(|open_wsl: &zed_actions::wsl_actions::OpenFolderInWsl, cx| {
 220        let create_new_window = open_wsl.create_new_window;
 221        with_active_or_new_workspace(cx, move |workspace, window, cx| {
 222            use gpui::PathPromptOptions;
 223            use project::DirectoryLister;
 224
 225            let paths = workspace.prompt_for_open_path(
 226                PathPromptOptions {
 227                    files: true,
 228                    directories: true,
 229                    multiple: false,
 230                    prompt: None,
 231                },
 232                DirectoryLister::Local(
 233                    workspace.project().clone(),
 234                    workspace.app_state().fs.clone(),
 235                ),
 236                window,
 237                cx,
 238            );
 239
 240            let app_state = workspace.app_state().clone();
 241            let window_handle = window.window_handle().downcast::<MultiWorkspace>();
 242
 243            cx.spawn_in(window, async move |workspace, cx| {
 244                use util::paths::SanitizedPath;
 245
 246                let Some(paths) = paths.await.log_err().flatten() else {
 247                    return;
 248                };
 249
 250                let wsl_path = paths
 251                    .iter()
 252                    .find_map(util::paths::WslPath::from_path);
 253
 254                if let Some(util::paths::WslPath { distro, path }) = wsl_path {
 255                    use remote::WslConnectionOptions;
 256
 257                    let connection_options = RemoteConnectionOptions::Wsl(WslConnectionOptions {
 258                        distro_name: distro.to_string(),
 259                        user: None,
 260                    });
 261
 262                    let replace_window = match create_new_window {
 263                        false => window_handle,
 264                        true => None,
 265                    };
 266
 267                    let open_options = workspace::OpenOptions {
 268                        replace_window,
 269                        ..Default::default()
 270                    };
 271
 272                    open_remote_project(connection_options, vec![path.into()], app_state, open_options, cx).await.log_err();
 273                    return;
 274                }
 275
 276                let paths = paths
 277                    .into_iter()
 278                    .filter_map(|path| SanitizedPath::new(&path).local_to_wsl())
 279                    .collect::<Vec<_>>();
 280
 281                if paths.is_empty() {
 282                    let message = indoc::indoc! { r#"
 283                        Invalid path specified when trying to open a folder inside WSL.
 284
 285                        Please note that Zed currently does not support opening network share folders inside wsl.
 286                    "#};
 287
 288                    let _ = cx.prompt(gpui::PromptLevel::Critical, "Invalid path", Some(&message), &["Ok"]).await;
 289                    return;
 290                }
 291
 292                workspace.update_in(cx, |workspace, window, cx| {
 293                    workspace.toggle_modal(window, cx, |window, cx| {
 294                        crate::wsl_picker::WslOpenModal::new(paths, create_new_window, window, cx)
 295                    });
 296                }).log_err();
 297            })
 298            .detach();
 299        });
 300    });
 301
 302    #[cfg(target_os = "windows")]
 303    cx.on_action(|open_wsl: &zed_actions::wsl_actions::OpenWsl, cx| {
 304        let create_new_window = open_wsl.create_new_window;
 305        with_active_or_new_workspace(cx, move |workspace, window, cx| {
 306            let handle = cx.entity().downgrade();
 307            let fs = workspace.project().read(cx).fs().clone();
 308            workspace.toggle_modal(window, cx, |window, cx| {
 309                RemoteServerProjects::wsl(create_new_window, fs, window, handle, cx)
 310            });
 311        });
 312    });
 313
 314    #[cfg(target_os = "windows")]
 315    cx.on_action(|open_wsl: &remote::OpenWslPath, cx| {
 316        let open_wsl = open_wsl.clone();
 317        with_active_or_new_workspace(cx, move |workspace, window, cx| {
 318            let fs = workspace.project().read(cx).fs().clone();
 319            add_wsl_distro(fs, &open_wsl.distro, cx);
 320            let open_options = OpenOptions {
 321                replace_window: window.window_handle().downcast::<MultiWorkspace>(),
 322                ..Default::default()
 323            };
 324
 325            let app_state = workspace.app_state().clone();
 326
 327            cx.spawn_in(window, async move |_, cx| {
 328                open_remote_project(
 329                    RemoteConnectionOptions::Wsl(open_wsl.distro.clone()),
 330                    open_wsl.paths,
 331                    app_state,
 332                    open_options,
 333                    cx,
 334                )
 335                .await
 336            })
 337            .detach();
 338        });
 339    });
 340
 341    cx.on_action(|open_recent: &OpenRecent, cx| {
 342        let create_new_window = open_recent.create_new_window;
 343
 344        match cx
 345            .active_window()
 346            .and_then(|w| w.downcast::<MultiWorkspace>())
 347        {
 348            Some(multi_workspace) => {
 349                cx.defer(move |cx| {
 350                    multi_workspace
 351                        .update(cx, |multi_workspace, window, cx| {
 352                            let sibling_workspace_ids: HashSet<WorkspaceId> = multi_workspace
 353                                .workspaces()
 354                                .iter()
 355                                .filter_map(|ws| ws.read(cx).database_id())
 356                                .collect();
 357
 358                            let workspace = multi_workspace.workspace().clone();
 359                            workspace.update(cx, |workspace, cx| {
 360                                let Some(recent_projects) =
 361                                    workspace.active_modal::<RecentProjects>(cx)
 362                                else {
 363                                    let focus_handle = workspace.focus_handle(cx);
 364                                    RecentProjects::open(
 365                                        workspace,
 366                                        create_new_window,
 367                                        sibling_workspace_ids,
 368                                        window,
 369                                        focus_handle,
 370                                        cx,
 371                                    );
 372                                    return;
 373                                };
 374
 375                                recent_projects.update(cx, |recent_projects, cx| {
 376                                    recent_projects
 377                                        .picker
 378                                        .update(cx, |picker, cx| picker.cycle_selection(window, cx))
 379                                });
 380                            });
 381                        })
 382                        .log_err();
 383                });
 384            }
 385            None => {
 386                with_active_or_new_workspace(cx, move |workspace, window, cx| {
 387                    let Some(recent_projects) = workspace.active_modal::<RecentProjects>(cx) else {
 388                        let focus_handle = workspace.focus_handle(cx);
 389                        RecentProjects::open(
 390                            workspace,
 391                            create_new_window,
 392                            HashSet::new(),
 393                            window,
 394                            focus_handle,
 395                            cx,
 396                        );
 397                        return;
 398                    };
 399
 400                    recent_projects.update(cx, |recent_projects, cx| {
 401                        recent_projects
 402                            .picker
 403                            .update(cx, |picker, cx| picker.cycle_selection(window, cx))
 404                    });
 405                });
 406            }
 407        }
 408    });
 409    cx.on_action(|open_remote: &OpenRemote, cx| {
 410        let from_existing_connection = open_remote.from_existing_connection;
 411        let create_new_window = open_remote.create_new_window;
 412        with_active_or_new_workspace(cx, move |workspace, window, cx| {
 413            if from_existing_connection {
 414                cx.propagate();
 415                return;
 416            }
 417            let handle = cx.entity().downgrade();
 418            let fs = workspace.project().read(cx).fs().clone();
 419            workspace.toggle_modal(window, cx, |window, cx| {
 420                RemoteServerProjects::new(create_new_window, fs, window, handle, cx)
 421            })
 422        });
 423    });
 424
 425    cx.observe_new(DisconnectedOverlay::register).detach();
 426
 427    cx.on_action(|_: &OpenDevContainer, cx| {
 428        with_active_or_new_workspace(cx, move |workspace, window, cx| {
 429            if !workspace.project().read(cx).is_local() {
 430                cx.spawn_in(window, async move |_, cx| {
 431                    cx.prompt(
 432                        gpui::PromptLevel::Critical,
 433                        "Cannot open Dev Container from remote project",
 434                        None,
 435                        &["Ok"],
 436                    )
 437                    .await
 438                    .ok();
 439                })
 440                .detach();
 441                return;
 442            }
 443
 444            let fs = workspace.project().read(cx).fs().clone();
 445            let configs = find_devcontainer_configs(workspace, cx);
 446            let app_state = workspace.app_state().clone();
 447            let dev_container_context = DevContainerContext::from_workspace(workspace, cx);
 448            let handle = cx.entity().downgrade();
 449            workspace.toggle_modal(window, cx, |window, cx| {
 450                RemoteServerProjects::new_dev_container(
 451                    fs,
 452                    configs,
 453                    app_state,
 454                    dev_container_context,
 455                    window,
 456                    handle,
 457                    cx,
 458                )
 459            });
 460        });
 461    });
 462
 463    // Subscribe to worktree additions to suggest opening the project in a dev container
 464    cx.observe_new(
 465        |workspace: &mut Workspace, window: Option<&mut Window>, cx: &mut Context<Workspace>| {
 466            let Some(window) = window else {
 467                return;
 468            };
 469            cx.subscribe_in(
 470                workspace.project(),
 471                window,
 472                move |_, project, event, window, cx| {
 473                    if let project::Event::WorktreeUpdatedEntries(worktree_id, updated_entries) =
 474                        event
 475                    {
 476                        dev_container_suggest::suggest_on_worktree_updated(
 477                            *worktree_id,
 478                            updated_entries,
 479                            project,
 480                            window,
 481                            cx,
 482                        );
 483                    }
 484                },
 485            )
 486            .detach();
 487        },
 488    )
 489    .detach();
 490}
 491
 492#[cfg(target_os = "windows")]
 493pub fn add_wsl_distro(
 494    fs: Arc<dyn project::Fs>,
 495    connection_options: &remote::WslConnectionOptions,
 496    cx: &App,
 497) {
 498    use gpui::ReadGlobal;
 499    use settings::SettingsStore;
 500
 501    let distro_name = connection_options.distro_name.clone();
 502    let user = connection_options.user.clone();
 503    SettingsStore::global(cx).update_settings_file(fs, move |setting, _| {
 504        let connections = setting
 505            .remote
 506            .wsl_connections
 507            .get_or_insert(Default::default());
 508
 509        if !connections
 510            .iter()
 511            .any(|conn| conn.distro_name == distro_name && conn.user == user)
 512        {
 513            use std::collections::BTreeSet;
 514
 515            connections.push(settings::WslConnection {
 516                distro_name,
 517                user,
 518                projects: BTreeSet::new(),
 519            })
 520        }
 521    });
 522}
 523
 524pub struct RecentProjects {
 525    pub picker: Entity<Picker<RecentProjectsDelegate>>,
 526    rem_width: f32,
 527    _subscription: Subscription,
 528}
 529
 530impl ModalView for RecentProjects {
 531    fn on_before_dismiss(
 532        &mut self,
 533        window: &mut Window,
 534        cx: &mut Context<Self>,
 535    ) -> workspace::DismissDecision {
 536        let submenu_focused = self.picker.update(cx, |picker, cx| {
 537            picker.delegate.actions_menu_handle.is_focused(window, cx)
 538        });
 539        workspace::DismissDecision::Dismiss(!submenu_focused)
 540    }
 541}
 542
 543impl RecentProjects {
 544    fn new(
 545        delegate: RecentProjectsDelegate,
 546        fs: Option<Arc<dyn Fs>>,
 547        rem_width: f32,
 548        window: &mut Window,
 549        cx: &mut Context<Self>,
 550    ) -> Self {
 551        let picker = cx.new(|cx| {
 552            Picker::list(delegate, window, cx)
 553                .list_measure_all()
 554                .show_scrollbar(true)
 555        });
 556
 557        let picker_focus_handle = picker.focus_handle(cx);
 558        picker.update(cx, |picker, _| {
 559            picker.delegate.focus_handle = picker_focus_handle;
 560        });
 561
 562        let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
 563        // We do not want to block the UI on a potentially lengthy call to DB, so we're gonna swap
 564        // out workspace locations once the future runs to completion.
 565        let db = WorkspaceDb::global(cx);
 566        cx.spawn_in(window, async move |this, cx| {
 567            let Some(fs) = fs else { return };
 568            let workspaces = db
 569                .recent_workspaces_on_disk(fs.as_ref())
 570                .await
 571                .log_err()
 572                .unwrap_or_default();
 573            let workspaces = workspace::resolve_worktree_workspaces(workspaces, fs.as_ref()).await;
 574            this.update_in(cx, move |this, window, cx| {
 575                this.picker.update(cx, move |picker, cx| {
 576                    picker.delegate.set_workspaces(workspaces);
 577                    picker.update_matches(picker.query(cx), window, cx)
 578                })
 579            })
 580            .ok();
 581        })
 582        .detach();
 583        Self {
 584            picker,
 585            rem_width,
 586            _subscription,
 587        }
 588    }
 589
 590    pub fn open(
 591        workspace: &mut Workspace,
 592        create_new_window: bool,
 593        sibling_workspace_ids: HashSet<WorkspaceId>,
 594        window: &mut Window,
 595        focus_handle: FocusHandle,
 596        cx: &mut Context<Workspace>,
 597    ) {
 598        let weak = cx.entity().downgrade();
 599        let open_folders = get_open_folders(workspace, cx);
 600        let project_connection_options = workspace.project().read(cx).remote_connection_options(cx);
 601        let fs = Some(workspace.app_state().fs.clone());
 602
 603        workspace.toggle_modal(window, cx, |window, cx| {
 604            let delegate = RecentProjectsDelegate::new(
 605                weak,
 606                create_new_window,
 607                focus_handle,
 608                open_folders,
 609                sibling_workspace_ids,
 610                project_connection_options,
 611                ProjectPickerStyle::Modal,
 612            );
 613
 614            Self::new(delegate, fs, 34., window, cx)
 615        })
 616    }
 617
 618    pub fn popover(
 619        workspace: WeakEntity<Workspace>,
 620        sibling_workspace_ids: HashSet<WorkspaceId>,
 621        create_new_window: bool,
 622        focus_handle: FocusHandle,
 623        window: &mut Window,
 624        cx: &mut App,
 625    ) -> Entity<Self> {
 626        let (open_folders, project_connection_options, fs) = workspace
 627            .upgrade()
 628            .map(|workspace| {
 629                let workspace = workspace.read(cx);
 630                (
 631                    get_open_folders(workspace, cx),
 632                    workspace.project().read(cx).remote_connection_options(cx),
 633                    Some(workspace.app_state().fs.clone()),
 634                )
 635            })
 636            .unwrap_or_else(|| (Vec::new(), None, None));
 637
 638        cx.new(|cx| {
 639            let delegate = RecentProjectsDelegate::new(
 640                workspace,
 641                create_new_window,
 642                focus_handle,
 643                open_folders,
 644                sibling_workspace_ids,
 645                project_connection_options,
 646                ProjectPickerStyle::Popover,
 647            );
 648            let list = Self::new(delegate, fs, 20., window, cx);
 649            list.picker.focus_handle(cx).focus(window, cx);
 650            list
 651        })
 652    }
 653
 654    fn handle_toggle_open_menu(
 655        &mut self,
 656        _: &ToggleActionsMenu,
 657        window: &mut Window,
 658        cx: &mut Context<Self>,
 659    ) {
 660        self.picker.update(cx, |picker, cx| {
 661            let menu_handle = &picker.delegate.actions_menu_handle;
 662            if menu_handle.is_deployed() {
 663                menu_handle.hide(cx);
 664            } else {
 665                menu_handle.show(window, cx);
 666            }
 667        });
 668    }
 669}
 670
 671impl EventEmitter<DismissEvent> for RecentProjects {}
 672
 673impl Focusable for RecentProjects {
 674    fn focus_handle(&self, cx: &App) -> FocusHandle {
 675        self.picker.focus_handle(cx)
 676    }
 677}
 678
 679impl Render for RecentProjects {
 680    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 681        v_flex()
 682            .key_context("RecentProjects")
 683            .on_action(cx.listener(Self::handle_toggle_open_menu))
 684            .w(rems(self.rem_width))
 685            .child(self.picker.clone())
 686    }
 687}
 688
 689pub struct RecentProjectsDelegate {
 690    workspace: WeakEntity<Workspace>,
 691    open_folders: Vec<OpenFolderEntry>,
 692    sibling_workspace_ids: HashSet<WorkspaceId>,
 693    workspaces: Vec<(
 694        WorkspaceId,
 695        SerializedWorkspaceLocation,
 696        PathList,
 697        DateTime<Utc>,
 698    )>,
 699    filtered_entries: Vec<ProjectPickerEntry>,
 700    selected_index: usize,
 701    render_paths: bool,
 702    create_new_window: bool,
 703    // Flag to reset index when there is a new query vs not reset index when user delete an item
 704    reset_selected_match_index: bool,
 705    has_any_non_local_projects: bool,
 706    project_connection_options: Option<RemoteConnectionOptions>,
 707    focus_handle: FocusHandle,
 708    style: ProjectPickerStyle,
 709    actions_menu_handle: PopoverMenuHandle<ContextMenu>,
 710}
 711
 712impl RecentProjectsDelegate {
 713    fn new(
 714        workspace: WeakEntity<Workspace>,
 715        create_new_window: bool,
 716        focus_handle: FocusHandle,
 717        open_folders: Vec<OpenFolderEntry>,
 718        sibling_workspace_ids: HashSet<WorkspaceId>,
 719        project_connection_options: Option<RemoteConnectionOptions>,
 720        style: ProjectPickerStyle,
 721    ) -> Self {
 722        let render_paths = style == ProjectPickerStyle::Modal;
 723        Self {
 724            workspace,
 725            open_folders,
 726            sibling_workspace_ids,
 727            workspaces: Vec::new(),
 728            filtered_entries: Vec::new(),
 729            selected_index: 0,
 730            create_new_window,
 731            render_paths,
 732            reset_selected_match_index: true,
 733            has_any_non_local_projects: project_connection_options.is_some(),
 734            project_connection_options,
 735            focus_handle,
 736            style,
 737            actions_menu_handle: PopoverMenuHandle::default(),
 738        }
 739    }
 740
 741    pub fn set_workspaces(
 742        &mut self,
 743        workspaces: Vec<(
 744            WorkspaceId,
 745            SerializedWorkspaceLocation,
 746            PathList,
 747            DateTime<Utc>,
 748        )>,
 749    ) {
 750        self.workspaces = workspaces;
 751        let has_non_local_recent = !self
 752            .workspaces
 753            .iter()
 754            .all(|(_, location, _, _)| matches!(location, SerializedWorkspaceLocation::Local));
 755        self.has_any_non_local_projects =
 756            self.project_connection_options.is_some() || has_non_local_recent;
 757    }
 758}
 759impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
 760impl PickerDelegate for RecentProjectsDelegate {
 761    type ListItem = AnyElement;
 762
 763    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 764        "Search projects…".into()
 765    }
 766
 767    fn render_editor(
 768        &self,
 769        editor: &Arc<dyn ErasedEditor>,
 770        window: &mut Window,
 771        cx: &mut Context<Picker<Self>>,
 772    ) -> Div {
 773        h_flex()
 774            .flex_none()
 775            .h_9()
 776            .px_2p5()
 777            .justify_between()
 778            .border_b_1()
 779            .border_color(cx.theme().colors().border_variant)
 780            .child(editor.render(window, cx))
 781    }
 782
 783    fn match_count(&self) -> usize {
 784        self.filtered_entries.len()
 785    }
 786
 787    fn selected_index(&self) -> usize {
 788        self.selected_index
 789    }
 790
 791    fn set_selected_index(
 792        &mut self,
 793        ix: usize,
 794        _window: &mut Window,
 795        _cx: &mut Context<Picker<Self>>,
 796    ) {
 797        self.selected_index = ix;
 798    }
 799
 800    fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
 801        matches!(
 802            self.filtered_entries.get(ix),
 803            Some(
 804                ProjectPickerEntry::OpenFolder { .. }
 805                    | ProjectPickerEntry::OpenProject(_)
 806                    | ProjectPickerEntry::RecentProject(_)
 807            )
 808        )
 809    }
 810
 811    fn update_matches(
 812        &mut self,
 813        query: String,
 814        _: &mut Window,
 815        cx: &mut Context<Picker<Self>>,
 816    ) -> gpui::Task<()> {
 817        let query = query.trim_start();
 818        let smart_case = query.chars().any(|c| c.is_uppercase());
 819        let is_empty_query = query.is_empty();
 820
 821        let folder_matches = if self.open_folders.is_empty() {
 822            Vec::new()
 823        } else {
 824            let candidates: Vec<_> = self
 825                .open_folders
 826                .iter()
 827                .enumerate()
 828                .map(|(id, folder)| StringMatchCandidate::new(id, folder.name.as_ref()))
 829                .collect();
 830
 831            smol::block_on(fuzzy::match_strings(
 832                &candidates,
 833                query,
 834                smart_case,
 835                true,
 836                100,
 837                &Default::default(),
 838                cx.background_executor().clone(),
 839            ))
 840        };
 841
 842        let sibling_candidates: Vec<_> = self
 843            .workspaces
 844            .iter()
 845            .enumerate()
 846            .filter(|(_, (id, _, _, _))| self.is_sibling_workspace(*id, cx))
 847            .map(|(id, (_, _, paths, _))| {
 848                let combined_string = paths
 849                    .ordered_paths()
 850                    .map(|path| path.compact().to_string_lossy().into_owned())
 851                    .collect::<Vec<_>>()
 852                    .join("");
 853                StringMatchCandidate::new(id, &combined_string)
 854            })
 855            .collect();
 856
 857        let mut sibling_matches = smol::block_on(fuzzy::match_strings(
 858            &sibling_candidates,
 859            query,
 860            smart_case,
 861            true,
 862            100,
 863            &Default::default(),
 864            cx.background_executor().clone(),
 865        ));
 866        sibling_matches.sort_unstable_by(|a, b| {
 867            b.score
 868                .partial_cmp(&a.score)
 869                .unwrap_or(std::cmp::Ordering::Equal)
 870                .then_with(|| a.candidate_id.cmp(&b.candidate_id))
 871        });
 872
 873        // Build candidates for recent projects (not current, not sibling, not open folder)
 874        let recent_candidates: Vec<_> = self
 875            .workspaces
 876            .iter()
 877            .enumerate()
 878            .filter(|(_, (id, _, paths, _))| self.is_valid_recent_candidate(*id, paths, cx))
 879            .map(|(id, (_, _, paths, _))| {
 880                let combined_string = paths
 881                    .ordered_paths()
 882                    .map(|path| path.compact().to_string_lossy().into_owned())
 883                    .collect::<Vec<_>>()
 884                    .join("");
 885                StringMatchCandidate::new(id, &combined_string)
 886            })
 887            .collect();
 888
 889        let mut recent_matches = smol::block_on(fuzzy::match_strings(
 890            &recent_candidates,
 891            query,
 892            smart_case,
 893            true,
 894            100,
 895            &Default::default(),
 896            cx.background_executor().clone(),
 897        ));
 898        recent_matches.sort_unstable_by(|a, b| {
 899            b.score
 900                .partial_cmp(&a.score)
 901                .unwrap_or(std::cmp::Ordering::Equal)
 902                .then_with(|| a.candidate_id.cmp(&b.candidate_id))
 903        });
 904
 905        let mut entries = Vec::new();
 906
 907        if !self.open_folders.is_empty() {
 908            let matched_folders: Vec<_> = if is_empty_query {
 909                (0..self.open_folders.len())
 910                    .map(|i| (i, Vec::new()))
 911                    .collect()
 912            } else {
 913                folder_matches
 914                    .iter()
 915                    .map(|m| (m.candidate_id, m.positions.clone()))
 916                    .collect()
 917            };
 918
 919            for (index, positions) in matched_folders {
 920                entries.push(ProjectPickerEntry::OpenFolder { index, positions });
 921            }
 922        }
 923
 924        let has_siblings_to_show = if is_empty_query {
 925            !sibling_candidates.is_empty()
 926        } else {
 927            !sibling_matches.is_empty()
 928        };
 929
 930        if has_siblings_to_show {
 931            entries.push(ProjectPickerEntry::Header("Open on This Window".into()));
 932
 933            if is_empty_query {
 934                for (id, (workspace_id, _, _, _)) in self.workspaces.iter().enumerate() {
 935                    if self.is_sibling_workspace(*workspace_id, cx) {
 936                        entries.push(ProjectPickerEntry::OpenProject(StringMatch {
 937                            candidate_id: id,
 938                            score: 0.0,
 939                            positions: Vec::new(),
 940                            string: String::new(),
 941                        }));
 942                    }
 943                }
 944            } else {
 945                for m in sibling_matches {
 946                    entries.push(ProjectPickerEntry::OpenProject(m));
 947                }
 948            }
 949        }
 950
 951        let has_recent_to_show = if is_empty_query {
 952            !recent_candidates.is_empty()
 953        } else {
 954            !recent_matches.is_empty()
 955        };
 956
 957        if has_recent_to_show {
 958            entries.push(ProjectPickerEntry::Header("Recent Projects".into()));
 959
 960            if is_empty_query {
 961                for (id, (workspace_id, _, paths, _)) in self.workspaces.iter().enumerate() {
 962                    if self.is_valid_recent_candidate(*workspace_id, paths, cx) {
 963                        entries.push(ProjectPickerEntry::RecentProject(StringMatch {
 964                            candidate_id: id,
 965                            score: 0.0,
 966                            positions: Vec::new(),
 967                            string: String::new(),
 968                        }));
 969                    }
 970                }
 971            } else {
 972                for m in recent_matches {
 973                    entries.push(ProjectPickerEntry::RecentProject(m));
 974                }
 975            }
 976        }
 977
 978        self.filtered_entries = entries;
 979
 980        if self.reset_selected_match_index {
 981            self.selected_index = self
 982                .filtered_entries
 983                .iter()
 984                .position(|e| !matches!(e, ProjectPickerEntry::Header(_)))
 985                .unwrap_or(0);
 986        }
 987        self.reset_selected_match_index = true;
 988        Task::ready(())
 989    }
 990
 991    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
 992        match self.filtered_entries.get(self.selected_index) {
 993            Some(ProjectPickerEntry::OpenFolder { index, .. }) => {
 994                let Some(folder) = self.open_folders.get(*index) else {
 995                    return;
 996                };
 997                let worktree_id = folder.worktree_id;
 998                if let Some(workspace) = self.workspace.upgrade() {
 999                    workspace.update(cx, |workspace, cx| {
1000                        workspace.set_active_worktree_override(Some(worktree_id), cx);
1001                    });
1002                }
1003                cx.emit(DismissEvent);
1004            }
1005            Some(ProjectPickerEntry::OpenProject(selected_match)) => {
1006                let Some((workspace_id, _, _, _)) =
1007                    self.workspaces.get(selected_match.candidate_id)
1008                else {
1009                    return;
1010                };
1011                let workspace_id = *workspace_id;
1012
1013                if let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() {
1014                    cx.defer(move |cx| {
1015                        handle
1016                            .update(cx, |multi_workspace, _window, cx| {
1017                                let workspace = multi_workspace
1018                                    .workspaces()
1019                                    .iter()
1020                                    .find(|ws| ws.read(cx).database_id() == Some(workspace_id))
1021                                    .cloned();
1022                                if let Some(workspace) = workspace {
1023                                    multi_workspace.activate(workspace, cx);
1024                                }
1025                            })
1026                            .log_err();
1027                    });
1028                }
1029                cx.emit(DismissEvent);
1030            }
1031            Some(ProjectPickerEntry::RecentProject(selected_match)) => {
1032                let Some(workspace) = self.workspace.upgrade() else {
1033                    return;
1034                };
1035                let Some((
1036                    candidate_workspace_id,
1037                    candidate_workspace_location,
1038                    candidate_workspace_paths,
1039                    _,
1040                )) = self.workspaces.get(selected_match.candidate_id)
1041                else {
1042                    return;
1043                };
1044
1045                let replace_current_window = self.create_new_window == secondary;
1046                let candidate_workspace_id = *candidate_workspace_id;
1047                let candidate_workspace_location = candidate_workspace_location.clone();
1048                let candidate_workspace_paths = candidate_workspace_paths.clone();
1049
1050                workspace.update(cx, |workspace, cx| {
1051                    if workspace.database_id() == Some(candidate_workspace_id) {
1052                        return;
1053                    }
1054                    match candidate_workspace_location {
1055                        SerializedWorkspaceLocation::Local => {
1056                            let paths = candidate_workspace_paths.paths().to_vec();
1057                            if replace_current_window {
1058                                if let Some(handle) =
1059                                    window.window_handle().downcast::<MultiWorkspace>()
1060                                {
1061                                    cx.defer(move |cx| {
1062                                        if let Some(task) = handle
1063                                            .update(cx, |multi_workspace, window, cx| {
1064                                                multi_workspace.open_project(paths, window, cx)
1065                                            })
1066                                            .log_err()
1067                                        {
1068                                            task.detach_and_log_err(cx);
1069                                        }
1070                                    });
1071                                }
1072                                return;
1073                            } else {
1074                                workspace
1075                                    .open_workspace_for_paths(false, paths, window, cx)
1076                                    .detach_and_prompt_err(
1077                                        "Failed to open project",
1078                                        window,
1079                                        cx,
1080                                        |_, _, _| None,
1081                                    );
1082                            }
1083                        }
1084                        SerializedWorkspaceLocation::Remote(mut connection) => {
1085                            let app_state = workspace.app_state().clone();
1086                            let replace_window = if replace_current_window {
1087                                window.window_handle().downcast::<MultiWorkspace>()
1088                            } else {
1089                                None
1090                            };
1091                            let open_options = OpenOptions {
1092                                replace_window,
1093                                ..Default::default()
1094                            };
1095                            if let RemoteConnectionOptions::Ssh(connection) = &mut connection {
1096                                RemoteSettings::get_global(cx)
1097                                    .fill_connection_options_from_settings(connection);
1098                            };
1099                            let paths = candidate_workspace_paths.paths().to_vec();
1100                            cx.spawn_in(window, async move |_, cx| {
1101                                open_remote_project(
1102                                    connection.clone(),
1103                                    paths,
1104                                    app_state,
1105                                    open_options,
1106                                    cx,
1107                                )
1108                                .await
1109                            })
1110                            .detach_and_prompt_err(
1111                                "Failed to open project",
1112                                window,
1113                                cx,
1114                                |_, _, _| None,
1115                            );
1116                        }
1117                    }
1118                });
1119                cx.emit(DismissEvent);
1120            }
1121            _ => {}
1122        }
1123    }
1124
1125    fn dismissed(&mut self, _window: &mut Window, _: &mut Context<Picker<Self>>) {}
1126
1127    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
1128        let text = if self.workspaces.is_empty() && self.open_folders.is_empty() {
1129            "Recently opened projects will show up here".into()
1130        } else {
1131            "No matches".into()
1132        };
1133        Some(text)
1134    }
1135
1136    fn render_match(
1137        &self,
1138        ix: usize,
1139        selected: bool,
1140        window: &mut Window,
1141        cx: &mut Context<Picker<Self>>,
1142    ) -> Option<Self::ListItem> {
1143        match self.filtered_entries.get(ix)? {
1144            ProjectPickerEntry::Header(title) => Some(
1145                v_flex()
1146                    .w_full()
1147                    .gap_1()
1148                    .when(ix > 0, |this| this.mt_1().child(Divider::horizontal()))
1149                    .child(ListSubHeader::new(title.clone()).inset(true))
1150                    .into_any_element(),
1151            ),
1152            ProjectPickerEntry::OpenFolder { index, positions } => {
1153                let folder = self.open_folders.get(*index)?;
1154                let name = folder.name.clone();
1155                let path = folder.path.compact();
1156                let branch = folder.branch.clone();
1157                let is_active = folder.is_active;
1158                let worktree_id = folder.worktree_id;
1159                let positions = positions.clone();
1160                let show_path = self.style == ProjectPickerStyle::Modal;
1161
1162                let secondary_actions = h_flex()
1163                    .gap_1()
1164                    .child(
1165                        IconButton::new(("remove-folder", worktree_id.to_usize()), IconName::Close)
1166                            .icon_size(IconSize::Small)
1167                            .tooltip(Tooltip::text("Remove Folder from Workspace"))
1168                            .on_click(cx.listener(move |picker, _, window, cx| {
1169                                let Some(workspace) = picker.delegate.workspace.upgrade() else {
1170                                    return;
1171                                };
1172                                workspace.update(cx, |workspace, cx| {
1173                                    let project = workspace.project().clone();
1174                                    project.update(cx, |project, cx| {
1175                                        project.remove_worktree(worktree_id, cx);
1176                                    });
1177                                });
1178                                picker.delegate.open_folders =
1179                                    get_open_folders(workspace.read(cx), cx);
1180                                let query = picker.query(cx);
1181                                picker.update_matches(query, window, cx);
1182                            })),
1183                    )
1184                    .into_any_element();
1185
1186                let icon = icon_for_remote_connection(self.project_connection_options.as_ref());
1187
1188                Some(
1189                    ListItem::new(ix)
1190                        .toggle_state(selected)
1191                        .inset(true)
1192                        .spacing(ListItemSpacing::Sparse)
1193                        .child(
1194                            h_flex()
1195                                .id("open_folder_item")
1196                                .gap_3()
1197                                .flex_grow()
1198                                .when(self.has_any_non_local_projects, |this| {
1199                                    this.child(Icon::new(icon).color(Color::Muted))
1200                                })
1201                                .child(
1202                                    v_flex()
1203                                        .child(
1204                                            h_flex()
1205                                                .gap_1()
1206                                                .child({
1207                                                    let highlighted = HighlightedMatch {
1208                                                        text: name.to_string(),
1209                                                        highlight_positions: positions,
1210                                                        color: Color::Default,
1211                                                    };
1212                                                    highlighted.render(window, cx)
1213                                                })
1214                                                .when_some(branch, |this, branch| {
1215                                                    this.child(
1216                                                        Label::new(branch).color(Color::Muted),
1217                                                    )
1218                                                })
1219                                                .when(is_active, |this| {
1220                                                    this.child(
1221                                                        Icon::new(IconName::Check)
1222                                                            .size(IconSize::Small)
1223                                                            .color(Color::Accent),
1224                                                    )
1225                                                }),
1226                                        )
1227                                        .when(show_path, |this| {
1228                                            this.child(
1229                                                Label::new(path.to_string_lossy().to_string())
1230                                                    .size(LabelSize::Small)
1231                                                    .color(Color::Muted),
1232                                            )
1233                                        }),
1234                                )
1235                                .when(!show_path, |this| {
1236                                    this.tooltip(Tooltip::text(path.to_string_lossy().to_string()))
1237                                }),
1238                        )
1239                        .map(|el| {
1240                            if self.selected_index == ix {
1241                                el.end_slot(secondary_actions)
1242                            } else {
1243                                el.end_hover_slot(secondary_actions)
1244                            }
1245                        })
1246                        .into_any_element(),
1247                )
1248            }
1249            ProjectPickerEntry::OpenProject(hit) => {
1250                let (workspace_id, location, paths, _) = self.workspaces.get(hit.candidate_id)?;
1251                let workspace_id = *workspace_id;
1252                let ordered_paths: Vec<_> = paths
1253                    .ordered_paths()
1254                    .map(|p| p.compact().to_string_lossy().to_string())
1255                    .collect();
1256                let tooltip_path: SharedString = match &location {
1257                    SerializedWorkspaceLocation::Remote(options) => {
1258                        let host = options.display_name();
1259                        if ordered_paths.len() == 1 {
1260                            format!("{} ({})", ordered_paths[0], host).into()
1261                        } else {
1262                            format!("{}\n({})", ordered_paths.join("\n"), host).into()
1263                        }
1264                    }
1265                    _ => ordered_paths.join("\n").into(),
1266                };
1267
1268                let mut path_start_offset = 0;
1269                let (match_labels, paths): (Vec<_>, Vec<_>) = paths
1270                    .ordered_paths()
1271                    .map(|p| p.compact())
1272                    .map(|path| {
1273                        let highlighted_text =
1274                            highlights_for_path(path.as_ref(), &hit.positions, path_start_offset);
1275                        path_start_offset += highlighted_text.1.text.len();
1276                        highlighted_text
1277                    })
1278                    .unzip();
1279
1280                let prefix = match &location {
1281                    SerializedWorkspaceLocation::Remote(options) => {
1282                        Some(SharedString::from(options.display_name()))
1283                    }
1284                    _ => None,
1285                };
1286
1287                let highlighted_match = HighlightedMatchWithPaths {
1288                    prefix,
1289                    match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "),
1290                    paths,
1291                };
1292
1293                let icon = icon_for_remote_connection(match location {
1294                    SerializedWorkspaceLocation::Local => None,
1295                    SerializedWorkspaceLocation::Remote(options) => Some(options),
1296                });
1297
1298                let secondary_actions = h_flex()
1299                    .gap_1()
1300                    .child(
1301                        IconButton::new("remove_open_project", IconName::Close)
1302                            .icon_size(IconSize::Small)
1303                            .tooltip(Tooltip::text("Remove Project from Window"))
1304                            .on_click(cx.listener(move |picker, _, window, cx| {
1305                                cx.stop_propagation();
1306                                window.prevent_default();
1307                                picker
1308                                    .delegate
1309                                    .remove_sibling_workspace(workspace_id, window, cx);
1310                                let query = picker.query(cx);
1311                                picker.update_matches(query, window, cx);
1312                            })),
1313                    )
1314                    .into_any_element();
1315
1316                Some(
1317                    ListItem::new(ix)
1318                        .toggle_state(selected)
1319                        .inset(true)
1320                        .spacing(ListItemSpacing::Sparse)
1321                        .child(
1322                            h_flex()
1323                                .id("open_project_info_container")
1324                                .gap_3()
1325                                .flex_grow()
1326                                .when(self.has_any_non_local_projects, |this| {
1327                                    this.child(Icon::new(icon).color(Color::Muted))
1328                                })
1329                                .child({
1330                                    let mut highlighted = highlighted_match;
1331                                    if !self.render_paths {
1332                                        highlighted.paths.clear();
1333                                    }
1334                                    highlighted.render(window, cx)
1335                                })
1336                                .tooltip(Tooltip::text(tooltip_path)),
1337                        )
1338                        .map(|el| {
1339                            if self.selected_index == ix {
1340                                el.end_slot(secondary_actions)
1341                            } else {
1342                                el.end_hover_slot(secondary_actions)
1343                            }
1344                        })
1345                        .into_any_element(),
1346                )
1347            }
1348            ProjectPickerEntry::RecentProject(hit) => {
1349                let popover_style = matches!(self.style, ProjectPickerStyle::Popover);
1350                let (_, location, paths, _) = self.workspaces.get(hit.candidate_id)?;
1351                let is_local = matches!(location, SerializedWorkspaceLocation::Local);
1352                let paths_to_add = paths.paths().to_vec();
1353                let ordered_paths: Vec<_> = paths
1354                    .ordered_paths()
1355                    .map(|p| p.compact().to_string_lossy().to_string())
1356                    .collect();
1357                let tooltip_path: SharedString = match &location {
1358                    SerializedWorkspaceLocation::Remote(options) => {
1359                        let host = options.display_name();
1360                        if ordered_paths.len() == 1 {
1361                            format!("{} ({})", ordered_paths[0], host).into()
1362                        } else {
1363                            format!("{}\n({})", ordered_paths.join("\n"), host).into()
1364                        }
1365                    }
1366                    _ => ordered_paths.join("\n").into(),
1367                };
1368
1369                let mut path_start_offset = 0;
1370                let (match_labels, paths): (Vec<_>, Vec<_>) = paths
1371                    .ordered_paths()
1372                    .map(|p| p.compact())
1373                    .map(|path| {
1374                        let highlighted_text =
1375                            highlights_for_path(path.as_ref(), &hit.positions, path_start_offset);
1376                        path_start_offset += highlighted_text.1.text.len();
1377                        highlighted_text
1378                    })
1379                    .unzip();
1380
1381                let prefix = match &location {
1382                    SerializedWorkspaceLocation::Remote(options) => {
1383                        Some(SharedString::from(options.display_name()))
1384                    }
1385                    _ => None,
1386                };
1387
1388                let highlighted_match = HighlightedMatchWithPaths {
1389                    prefix,
1390                    match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "),
1391                    paths,
1392                };
1393
1394                let focus_handle = self.focus_handle.clone();
1395
1396                let secondary_actions = h_flex()
1397                    .gap_px()
1398                    .when(is_local, |this| {
1399                        this.child(
1400                            IconButton::new("add_to_workspace", IconName::FolderPlus)
1401                                .icon_size(IconSize::Small)
1402                                .tooltip(Tooltip::text("Add Project to this Workspace"))
1403                                .on_click({
1404                                    let paths_to_add = paths_to_add.clone();
1405                                    cx.listener(move |picker, _event, window, cx| {
1406                                        cx.stop_propagation();
1407                                        window.prevent_default();
1408                                        picker.delegate.add_project_to_workspace(
1409                                            paths_to_add.clone(),
1410                                            window,
1411                                            cx,
1412                                        );
1413                                    })
1414                                }),
1415                        )
1416                    })
1417                    .when(popover_style, |this| {
1418                        this.child(
1419                            IconButton::new("open_new_window", IconName::ArrowUpRight)
1420                                .icon_size(IconSize::XSmall)
1421                                .tooltip({
1422                                    move |_, cx| {
1423                                        Tooltip::for_action_in(
1424                                            "Open Project in New Window",
1425                                            &menu::SecondaryConfirm,
1426                                            &focus_handle,
1427                                            cx,
1428                                        )
1429                                    }
1430                                })
1431                                .on_click(cx.listener(move |this, _event, window, cx| {
1432                                    cx.stop_propagation();
1433                                    window.prevent_default();
1434                                    this.delegate.set_selected_index(ix, window, cx);
1435                                    this.delegate.confirm(true, window, cx);
1436                                })),
1437                        )
1438                    })
1439                    .child(
1440                        IconButton::new("delete", IconName::Close)
1441                            .icon_size(IconSize::Small)
1442                            .tooltip(Tooltip::text("Delete from Recent Projects"))
1443                            .on_click(cx.listener(move |this, _event, window, cx| {
1444                                cx.stop_propagation();
1445                                window.prevent_default();
1446                                this.delegate.delete_recent_project(ix, window, cx)
1447                            })),
1448                    )
1449                    .into_any_element();
1450
1451                let icon = icon_for_remote_connection(match location {
1452                    SerializedWorkspaceLocation::Local => None,
1453                    SerializedWorkspaceLocation::Remote(options) => Some(options),
1454                });
1455
1456                Some(
1457                    ListItem::new(ix)
1458                        .toggle_state(selected)
1459                        .inset(true)
1460                        .spacing(ListItemSpacing::Sparse)
1461                        .child(
1462                            h_flex()
1463                                .id("project_info_container")
1464                                .gap_3()
1465                                .flex_grow()
1466                                .when(self.has_any_non_local_projects, |this| {
1467                                    this.child(Icon::new(icon).color(Color::Muted))
1468                                })
1469                                .child({
1470                                    let mut highlighted = highlighted_match;
1471                                    if !self.render_paths {
1472                                        highlighted.paths.clear();
1473                                    }
1474                                    highlighted.render(window, cx)
1475                                })
1476                                .tooltip(Tooltip::text(tooltip_path)),
1477                        )
1478                        .map(|el| {
1479                            if self.selected_index == ix {
1480                                el.end_slot(secondary_actions)
1481                            } else {
1482                                el.end_hover_slot(secondary_actions)
1483                            }
1484                        })
1485                        .into_any_element(),
1486                )
1487            }
1488        }
1489    }
1490
1491    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
1492        let focus_handle = self.focus_handle.clone();
1493        let popover_style = matches!(self.style, ProjectPickerStyle::Popover);
1494        let is_already_open_entry = matches!(
1495            self.filtered_entries.get(self.selected_index),
1496            Some(ProjectPickerEntry::OpenFolder { .. } | ProjectPickerEntry::OpenProject(_))
1497        );
1498
1499        if popover_style {
1500            return Some(
1501                v_flex()
1502                    .flex_1()
1503                    .p_1p5()
1504                    .gap_1()
1505                    .border_t_1()
1506                    .border_color(cx.theme().colors().border_variant)
1507                    .child({
1508                        let open_action = workspace::Open {
1509                            create_new_window: self.create_new_window,
1510                        };
1511                        Button::new("open_local_folder", "Open Local Project")
1512                            .key_binding(KeyBinding::for_action_in(&open_action, &focus_handle, cx))
1513                            .on_click(move |_, window, cx| {
1514                                window.dispatch_action(open_action.boxed_clone(), cx)
1515                            })
1516                    })
1517                    .child(
1518                        Button::new("open_remote_folder", "Open Remote Project")
1519                            .key_binding(KeyBinding::for_action(
1520                                &OpenRemote {
1521                                    from_existing_connection: false,
1522                                    create_new_window: false,
1523                                },
1524                                cx,
1525                            ))
1526                            .on_click(|_, window, cx| {
1527                                window.dispatch_action(
1528                                    OpenRemote {
1529                                        from_existing_connection: false,
1530                                        create_new_window: false,
1531                                    }
1532                                    .boxed_clone(),
1533                                    cx,
1534                                )
1535                            }),
1536                    )
1537                    .into_any(),
1538            );
1539        }
1540
1541        Some(
1542            h_flex()
1543                .flex_1()
1544                .p_1p5()
1545                .gap_1()
1546                .justify_end()
1547                .border_t_1()
1548                .border_color(cx.theme().colors().border_variant)
1549                .map(|this| {
1550                    if is_already_open_entry {
1551                        this.child(
1552                            Button::new("activate", "Activate")
1553                                .key_binding(KeyBinding::for_action_in(
1554                                    &menu::Confirm,
1555                                    &focus_handle,
1556                                    cx,
1557                                ))
1558                                .on_click(|_, window, cx| {
1559                                    window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1560                                }),
1561                        )
1562                    } else {
1563                        this.child(
1564                            Button::new("open_new_window", "New Window")
1565                                .key_binding(KeyBinding::for_action_in(
1566                                    &menu::SecondaryConfirm,
1567                                    &focus_handle,
1568                                    cx,
1569                                ))
1570                                .on_click(|_, window, cx| {
1571                                    window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
1572                                }),
1573                        )
1574                        .child(
1575                            Button::new("open_here", "Open")
1576                                .key_binding(KeyBinding::for_action_in(
1577                                    &menu::Confirm,
1578                                    &focus_handle,
1579                                    cx,
1580                                ))
1581                                .on_click(|_, window, cx| {
1582                                    window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1583                                }),
1584                        )
1585                    }
1586                })
1587                .child(Divider::vertical())
1588                .child(
1589                    PopoverMenu::new("actions-menu-popover")
1590                        .with_handle(self.actions_menu_handle.clone())
1591                        .anchor(gpui::Corner::BottomRight)
1592                        .offset(gpui::Point {
1593                            x: px(0.0),
1594                            y: px(-2.0),
1595                        })
1596                        .trigger(
1597                            Button::new("actions-trigger", "Actions…")
1598                                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1599                                .key_binding(KeyBinding::for_action_in(
1600                                    &ToggleActionsMenu,
1601                                    &focus_handle,
1602                                    cx,
1603                                )),
1604                        )
1605                        .menu({
1606                            let focus_handle = focus_handle.clone();
1607                            let create_new_window = self.create_new_window;
1608
1609                            move |window, cx| {
1610                                Some(ContextMenu::build(window, cx, {
1611                                    let focus_handle = focus_handle.clone();
1612                                    move |menu, _, _| {
1613                                        menu.context(focus_handle)
1614                                            .action(
1615                                                "Open Local Project",
1616                                                workspace::Open { create_new_window }.boxed_clone(),
1617                                            )
1618                                            .action(
1619                                                "Open Remote Project",
1620                                                OpenRemote {
1621                                                    from_existing_connection: false,
1622                                                    create_new_window: false,
1623                                                }
1624                                                .boxed_clone(),
1625                                            )
1626                                    }
1627                                }))
1628                            }
1629                        }),
1630                )
1631                .into_any(),
1632        )
1633    }
1634}
1635
1636fn icon_for_remote_connection(options: Option<&RemoteConnectionOptions>) -> IconName {
1637    match options {
1638        None => IconName::Screen,
1639        Some(options) => match options {
1640            RemoteConnectionOptions::Ssh(_) => IconName::Server,
1641            RemoteConnectionOptions::Wsl(_) => IconName::Linux,
1642            RemoteConnectionOptions::Docker(_) => IconName::Box,
1643            #[cfg(any(test, feature = "test-support"))]
1644            RemoteConnectionOptions::Mock(_) => IconName::Server,
1645        },
1646    }
1647}
1648
1649// Compute the highlighted text for the name and path
1650fn highlights_for_path(
1651    path: &Path,
1652    match_positions: &Vec<usize>,
1653    path_start_offset: usize,
1654) -> (Option<HighlightedMatch>, HighlightedMatch) {
1655    let path_string = path.to_string_lossy();
1656    let path_text = path_string.to_string();
1657    let path_byte_len = path_text.len();
1658    // Get the subset of match highlight positions that line up with the given path.
1659    // Also adjusts them to start at the path start
1660    let path_positions = match_positions
1661        .iter()
1662        .copied()
1663        .skip_while(|position| *position < path_start_offset)
1664        .take_while(|position| *position < path_start_offset + path_byte_len)
1665        .map(|position| position - path_start_offset)
1666        .collect::<Vec<_>>();
1667
1668    // Again subset the highlight positions to just those that line up with the file_name
1669    // again adjusted to the start of the file_name
1670    let file_name_text_and_positions = path.file_name().map(|file_name| {
1671        let file_name_text = file_name.to_string_lossy().into_owned();
1672        let file_name_start_byte = path_byte_len - file_name_text.len();
1673        let highlight_positions = path_positions
1674            .iter()
1675            .copied()
1676            .skip_while(|position| *position < file_name_start_byte)
1677            .take_while(|position| *position < file_name_start_byte + file_name_text.len())
1678            .map(|position| position - file_name_start_byte)
1679            .collect::<Vec<_>>();
1680        HighlightedMatch {
1681            text: file_name_text,
1682            highlight_positions,
1683            color: Color::Default,
1684        }
1685    });
1686
1687    (
1688        file_name_text_and_positions,
1689        HighlightedMatch {
1690            text: path_text,
1691            highlight_positions: path_positions,
1692            color: Color::Default,
1693        },
1694    )
1695}
1696impl RecentProjectsDelegate {
1697    fn add_project_to_workspace(
1698        &mut self,
1699        paths: Vec<PathBuf>,
1700        window: &mut Window,
1701        cx: &mut Context<Picker<Self>>,
1702    ) {
1703        let Some(workspace) = self.workspace.upgrade() else {
1704            return;
1705        };
1706        let open_paths_task = workspace.update(cx, |workspace, cx| {
1707            workspace.open_paths(
1708                paths,
1709                OpenOptions {
1710                    visible: Some(OpenVisible::All),
1711                    ..Default::default()
1712                },
1713                None,
1714                window,
1715                cx,
1716            )
1717        });
1718        cx.spawn_in(window, async move |picker, cx| {
1719            let _result = open_paths_task.await;
1720            picker
1721                .update_in(cx, |picker, window, cx| {
1722                    let Some(workspace) = picker.delegate.workspace.upgrade() else {
1723                        return;
1724                    };
1725                    picker.delegate.open_folders = get_open_folders(workspace.read(cx), cx);
1726                    let query = picker.query(cx);
1727                    picker.update_matches(query, window, cx);
1728                })
1729                .ok();
1730        })
1731        .detach();
1732    }
1733
1734    fn delete_recent_project(
1735        &self,
1736        ix: usize,
1737        window: &mut Window,
1738        cx: &mut Context<Picker<Self>>,
1739    ) {
1740        if let Some(ProjectPickerEntry::RecentProject(selected_match)) =
1741            self.filtered_entries.get(ix)
1742        {
1743            let (workspace_id, _, _, _) = &self.workspaces[selected_match.candidate_id];
1744            let workspace_id = *workspace_id;
1745            let fs = self
1746                .workspace
1747                .upgrade()
1748                .map(|ws| ws.read(cx).app_state().fs.clone());
1749            let db = WorkspaceDb::global(cx);
1750            cx.spawn_in(window, async move |this, cx| {
1751                db.delete_workspace_by_id(workspace_id).await.log_err();
1752                let Some(fs) = fs else { return };
1753                let workspaces = db
1754                    .recent_workspaces_on_disk(fs.as_ref())
1755                    .await
1756                    .unwrap_or_default();
1757                let workspaces =
1758                    workspace::resolve_worktree_workspaces(workspaces, fs.as_ref()).await;
1759                this.update_in(cx, move |picker, window, cx| {
1760                    picker.delegate.set_workspaces(workspaces);
1761                    picker
1762                        .delegate
1763                        .set_selected_index(ix.saturating_sub(1), window, cx);
1764                    picker.delegate.reset_selected_match_index = false;
1765                    picker.update_matches(picker.query(cx), window, cx);
1766                    // After deleting a project, we want to update the history manager to reflect the change.
1767                    // But we do not emit a update event when user opens a project, because it's handled in `workspace::load_workspace`.
1768                    if let Some(history_manager) = HistoryManager::global(cx) {
1769                        history_manager
1770                            .update(cx, |this, cx| this.delete_history(workspace_id, cx));
1771                    }
1772                })
1773                .ok();
1774            })
1775            .detach();
1776        }
1777    }
1778
1779    fn remove_sibling_workspace(
1780        &mut self,
1781        workspace_id: WorkspaceId,
1782        window: &mut Window,
1783        cx: &mut Context<Picker<Self>>,
1784    ) {
1785        if let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() {
1786            cx.defer(move |cx| {
1787                handle
1788                    .update(cx, |multi_workspace, window, cx| {
1789                        let index = multi_workspace
1790                            .workspaces()
1791                            .iter()
1792                            .position(|ws| ws.read(cx).database_id() == Some(workspace_id));
1793                        if let Some(index) = index {
1794                            multi_workspace.remove_workspace(index, window, cx);
1795                        }
1796                    })
1797                    .log_err();
1798            });
1799        }
1800
1801        self.sibling_workspace_ids.remove(&workspace_id);
1802    }
1803
1804    fn is_current_workspace(
1805        &self,
1806        workspace_id: WorkspaceId,
1807        cx: &mut Context<Picker<Self>>,
1808    ) -> bool {
1809        if let Some(workspace) = self.workspace.upgrade() {
1810            let workspace = workspace.read(cx);
1811            if Some(workspace_id) == workspace.database_id() {
1812                return true;
1813            }
1814        }
1815
1816        false
1817    }
1818
1819    fn is_sibling_workspace(
1820        &self,
1821        workspace_id: WorkspaceId,
1822        cx: &mut Context<Picker<Self>>,
1823    ) -> bool {
1824        self.sibling_workspace_ids.contains(&workspace_id)
1825            && !self.is_current_workspace(workspace_id, cx)
1826    }
1827
1828    fn is_open_folder(&self, paths: &PathList) -> bool {
1829        if self.open_folders.is_empty() {
1830            return false;
1831        }
1832
1833        for workspace_path in paths.paths() {
1834            for open_folder in &self.open_folders {
1835                if workspace_path == &open_folder.path {
1836                    return true;
1837                }
1838            }
1839        }
1840
1841        false
1842    }
1843
1844    fn is_valid_recent_candidate(
1845        &self,
1846        workspace_id: WorkspaceId,
1847        paths: &PathList,
1848        cx: &mut Context<Picker<Self>>,
1849    ) -> bool {
1850        !self.is_current_workspace(workspace_id, cx)
1851            && !self.is_sibling_workspace(workspace_id, cx)
1852            && !self.is_open_folder(paths)
1853    }
1854}
1855
1856#[cfg(test)]
1857mod tests {
1858    use std::path::PathBuf;
1859
1860    use editor::Editor;
1861    use gpui::{TestAppContext, UpdateGlobal, WindowHandle};
1862
1863    use serde_json::json;
1864    use settings::SettingsStore;
1865    use util::path;
1866    use workspace::{AppState, open_paths};
1867
1868    use super::*;
1869
1870    #[gpui::test]
1871    async fn test_dirty_workspace_survives_when_opening_recent_project(cx: &mut TestAppContext) {
1872        let app_state = init_test(cx);
1873
1874        cx.update(|cx| {
1875            SettingsStore::update_global(cx, |store, cx| {
1876                store.update_user_settings(cx, |settings| {
1877                    settings
1878                        .session
1879                        .get_or_insert_default()
1880                        .restore_unsaved_buffers = Some(false)
1881                });
1882            });
1883        });
1884
1885        app_state
1886            .fs
1887            .as_fake()
1888            .insert_tree(
1889                path!("/dir"),
1890                json!({
1891                    "main.ts": "a"
1892                }),
1893            )
1894            .await;
1895        app_state
1896            .fs
1897            .as_fake()
1898            .insert_tree(path!("/test/path"), json!({}))
1899            .await;
1900        cx.update(|cx| {
1901            open_paths(
1902                &[PathBuf::from(path!("/dir/main.ts"))],
1903                app_state,
1904                workspace::OpenOptions::default(),
1905                cx,
1906            )
1907        })
1908        .await
1909        .unwrap();
1910        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1911
1912        let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
1913        multi_workspace
1914            .update(cx, |multi_workspace, _, cx| {
1915                assert!(!multi_workspace.workspace().read(cx).is_edited())
1916            })
1917            .unwrap();
1918
1919        let editor = multi_workspace
1920            .read_with(cx, |multi_workspace, cx| {
1921                multi_workspace
1922                    .workspace()
1923                    .read(cx)
1924                    .active_item(cx)
1925                    .unwrap()
1926                    .downcast::<Editor>()
1927                    .unwrap()
1928            })
1929            .unwrap();
1930        multi_workspace
1931            .update(cx, |_, window, cx| {
1932                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
1933            })
1934            .unwrap();
1935        multi_workspace
1936            .update(cx, |multi_workspace, _, cx| {
1937                assert!(
1938                    multi_workspace.workspace().read(cx).is_edited(),
1939                    "After inserting more text into the editor without saving, we should have a dirty project"
1940                )
1941            })
1942            .unwrap();
1943
1944        let recent_projects_picker = open_recent_projects(&multi_workspace, cx);
1945        multi_workspace
1946            .update(cx, |_, _, cx| {
1947                recent_projects_picker.update(cx, |picker, cx| {
1948                    assert_eq!(picker.query(cx), "");
1949                    let delegate = &mut picker.delegate;
1950                    delegate.set_workspaces(vec![(
1951                        WorkspaceId::default(),
1952                        SerializedWorkspaceLocation::Local,
1953                        PathList::new(&[path!("/test/path")]),
1954                        Utc::now(),
1955                    )]);
1956                    delegate.filtered_entries =
1957                        vec![ProjectPickerEntry::RecentProject(StringMatch {
1958                            candidate_id: 0,
1959                            score: 1.0,
1960                            positions: Vec::new(),
1961                            string: "fake candidate".to_string(),
1962                        })];
1963                });
1964            })
1965            .unwrap();
1966
1967        assert!(
1968            !cx.has_pending_prompt(),
1969            "Should have no pending prompt on dirty project before opening the new recent project"
1970        );
1971        let dirty_workspace = multi_workspace
1972            .read_with(cx, |multi_workspace, _cx| {
1973                multi_workspace.workspace().clone()
1974            })
1975            .unwrap();
1976
1977        cx.dispatch_action(*multi_workspace, menu::Confirm);
1978        cx.run_until_parked();
1979
1980        multi_workspace
1981            .update(cx, |multi_workspace, _, cx| {
1982                assert!(
1983                    multi_workspace
1984                        .workspace()
1985                        .read(cx)
1986                        .active_modal::<RecentProjects>(cx)
1987                        .is_none(),
1988                    "Should remove the modal after selecting new recent project"
1989                );
1990
1991                assert!(
1992                    multi_workspace.workspaces().len() >= 2,
1993                    "Should have at least 2 workspaces: the dirty one and the newly opened one"
1994                );
1995
1996                assert!(
1997                    multi_workspace.workspaces().contains(&dirty_workspace),
1998                    "The original dirty workspace should still be present"
1999                );
2000
2001                assert!(
2002                    dirty_workspace.read(cx).is_edited(),
2003                    "The original workspace should still be dirty"
2004                );
2005            })
2006            .unwrap();
2007
2008        assert!(
2009            !cx.has_pending_prompt(),
2010            "No save prompt in multi-workspace mode — dirty workspace survives in background"
2011        );
2012    }
2013
2014    fn open_recent_projects(
2015        multi_workspace: &WindowHandle<MultiWorkspace>,
2016        cx: &mut TestAppContext,
2017    ) -> Entity<Picker<RecentProjectsDelegate>> {
2018        cx.dispatch_action(
2019            (*multi_workspace).into(),
2020            OpenRecent {
2021                create_new_window: false,
2022            },
2023        );
2024        multi_workspace
2025            .update(cx, |multi_workspace, _, cx| {
2026                multi_workspace
2027                    .workspace()
2028                    .read(cx)
2029                    .active_modal::<RecentProjects>(cx)
2030                    .unwrap()
2031                    .read(cx)
2032                    .picker
2033                    .clone()
2034            })
2035            .unwrap()
2036    }
2037
2038    #[gpui::test]
2039    async fn test_open_dev_container_action_with_single_config(cx: &mut TestAppContext) {
2040        let app_state = init_test(cx);
2041
2042        app_state
2043            .fs
2044            .as_fake()
2045            .insert_tree(
2046                path!("/project"),
2047                json!({
2048                    ".devcontainer": {
2049                        "devcontainer.json": "{}"
2050                    },
2051                    "src": {
2052                        "main.rs": "fn main() {}"
2053                    }
2054                }),
2055            )
2056            .await;
2057
2058        cx.update(|cx| {
2059            open_paths(
2060                &[PathBuf::from(path!("/project"))],
2061                app_state,
2062                workspace::OpenOptions::default(),
2063                cx,
2064            )
2065        })
2066        .await
2067        .unwrap();
2068
2069        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2070        let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
2071
2072        cx.run_until_parked();
2073
2074        // This dispatch triggers with_active_or_new_workspace -> MultiWorkspace::update
2075        // -> Workspace::update -> toggle_modal -> new_dev_container.
2076        // Before the fix, this panicked with "cannot read workspace::Workspace while
2077        // it is already being updated" because new_dev_container and open_dev_container
2078        // tried to read the Workspace entity through a WeakEntity handle while it was
2079        // already leased by the outer update.
2080        cx.dispatch_action(*multi_workspace, OpenDevContainer);
2081
2082        multi_workspace
2083            .update(cx, |multi_workspace, _, cx| {
2084                let modal = multi_workspace
2085                    .workspace()
2086                    .read(cx)
2087                    .active_modal::<RemoteServerProjects>(cx);
2088                assert!(
2089                    modal.is_some(),
2090                    "Dev container modal should be open after dispatching OpenDevContainer"
2091                );
2092            })
2093            .unwrap();
2094    }
2095
2096    #[gpui::test]
2097    async fn test_open_dev_container_action_with_multiple_configs(cx: &mut TestAppContext) {
2098        let app_state = init_test(cx);
2099
2100        app_state
2101            .fs
2102            .as_fake()
2103            .insert_tree(
2104                path!("/project"),
2105                json!({
2106                    ".devcontainer": {
2107                        "rust": {
2108                            "devcontainer.json": "{}"
2109                        },
2110                        "python": {
2111                            "devcontainer.json": "{}"
2112                        }
2113                    },
2114                    "src": {
2115                        "main.rs": "fn main() {}"
2116                    }
2117                }),
2118            )
2119            .await;
2120
2121        cx.update(|cx| {
2122            open_paths(
2123                &[PathBuf::from(path!("/project"))],
2124                app_state,
2125                workspace::OpenOptions::default(),
2126                cx,
2127            )
2128        })
2129        .await
2130        .unwrap();
2131
2132        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2133        let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
2134
2135        cx.run_until_parked();
2136
2137        cx.dispatch_action(*multi_workspace, OpenDevContainer);
2138
2139        multi_workspace
2140            .update(cx, |multi_workspace, _, cx| {
2141                let modal = multi_workspace
2142                    .workspace()
2143                    .read(cx)
2144                    .active_modal::<RemoteServerProjects>(cx);
2145                assert!(
2146                    modal.is_some(),
2147                    "Dev container modal should be open after dispatching OpenDevContainer with multiple configs"
2148                );
2149            })
2150            .unwrap();
2151    }
2152
2153    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
2154        cx.update(|cx| {
2155            let state = AppState::test(cx);
2156            crate::init(cx);
2157            editor::init(cx);
2158            state
2159        })
2160    }
2161}