workspace.rs

   1pub mod dock;
   2pub mod item;
   3mod modal_layer;
   4pub mod notifications;
   5pub mod pane;
   6pub mod pane_group;
   7mod persistence;
   8pub mod searchable;
   9pub mod shared_screen;
  10mod status_bar;
  11pub mod tasks;
  12mod theme_preview;
  13mod toolbar;
  14mod workspace_settings;
  15
  16use anyhow::{anyhow, Context as _, Result};
  17use call::{call_settings::CallSettings, ActiveCall};
  18use client::{
  19    proto::{self, ErrorCode, PanelId, PeerId},
  20    ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
  21};
  22use collections::{hash_map, HashMap, HashSet};
  23use derive_more::{Deref, DerefMut};
  24use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
  25use futures::{
  26    channel::{
  27        mpsc::{self, UnboundedReceiver, UnboundedSender},
  28        oneshot,
  29    },
  30    future::try_join_all,
  31    Future, FutureExt, StreamExt,
  32};
  33use gpui::{
  34    action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
  35    transparent_black, Action, AnyView, AnyWeakView, AppContext, AsyncAppContext,
  36    AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
  37    EventEmitter, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView,
  38    Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge,
  39    Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, WindowHandle,
  40    WindowId, WindowOptions,
  41};
  42pub use item::{
  43    FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
  44    ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
  45};
  46use itertools::Itertools;
  47use language::{LanguageRegistry, Rope};
  48pub use modal_layer::*;
  49use node_runtime::NodeRuntime;
  50use notifications::{
  51    simple_message_notification::MessageNotification, DetachAndPromptErr, NotificationHandle,
  52};
  53pub use pane::*;
  54pub use pane_group::*;
  55pub use persistence::{
  56    model::{ItemId, LocalPaths, SerializedWorkspaceLocation},
  57    WorkspaceDb, DB as WORKSPACE_DB,
  58};
  59use persistence::{
  60    model::{SerializedSshProject, SerializedWorkspace},
  61    SerializedWindowBounds, DB,
  62};
  63use postage::stream::Stream;
  64use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree};
  65use remote::{ssh_session::ConnectionIdentifier, SshClientDelegate, SshConnectionOptions};
  66use schemars::JsonSchema;
  67use serde::Deserialize;
  68use session::AppSession;
  69use settings::Settings;
  70use shared_screen::SharedScreen;
  71use sqlez::{
  72    bindable::{Bind, Column, StaticColumnCount},
  73    statement::Statement,
  74};
  75use status_bar::StatusBar;
  76pub use status_bar::StatusItemView;
  77use std::{
  78    any::TypeId,
  79    borrow::Cow,
  80    cell::RefCell,
  81    cmp,
  82    collections::hash_map::DefaultHasher,
  83    env,
  84    hash::{Hash, Hasher},
  85    path::{Path, PathBuf},
  86    rc::Rc,
  87    sync::{atomic::AtomicUsize, Arc, LazyLock, Weak},
  88    time::Duration,
  89};
  90use task::SpawnInTerminal;
  91use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
  92pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
  93pub use ui;
  94use ui::prelude::*;
  95use util::{paths::SanitizedPath, serde::default_true, ResultExt, TryFutureExt};
  96use uuid::Uuid;
  97pub use workspace_settings::{
  98    AutosaveSetting, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
  99};
 100
 101use crate::notifications::NotificationId;
 102use crate::persistence::{
 103    model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
 104    SerializedAxis,
 105};
 106
 107static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
 108    env::var("ZED_WINDOW_SIZE")
 109        .ok()
 110        .as_deref()
 111        .and_then(parse_pixel_size_env_var)
 112});
 113
 114static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
 115    env::var("ZED_WINDOW_POSITION")
 116        .ok()
 117        .as_deref()
 118        .and_then(parse_pixel_position_env_var)
 119});
 120
 121actions!(assistant, [ShowConfiguration]);
 122
 123actions!(
 124    workspace,
 125    [
 126        ActivateNextPane,
 127        ActivatePreviousPane,
 128        ActivateNextWindow,
 129        ActivatePreviousWindow,
 130        AddFolderToProject,
 131        ClearAllNotifications,
 132        CloseAllDocks,
 133        CloseWindow,
 134        CopyPath,
 135        CopyRelativePath,
 136        Feedback,
 137        FollowNextCollaborator,
 138        MoveFocusedPanelToNextPosition,
 139        NewCenterTerminal,
 140        NewFile,
 141        NewFileSplitVertical,
 142        NewFileSplitHorizontal,
 143        NewSearch,
 144        NewTerminal,
 145        NewWindow,
 146        Open,
 147        OpenFiles,
 148        OpenInTerminal,
 149        ReloadActiveItem,
 150        SaveAs,
 151        SaveWithoutFormat,
 152        ToggleBottomDock,
 153        ToggleCenteredLayout,
 154        ToggleLeftDock,
 155        ToggleRightDock,
 156        ToggleZoom,
 157        Unfollow,
 158        Welcome,
 159    ]
 160);
 161
 162#[derive(Clone, PartialEq)]
 163pub struct OpenPaths {
 164    pub paths: Vec<PathBuf>,
 165}
 166
 167#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 168pub struct ActivatePane(pub usize);
 169
 170#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 171pub struct ActivatePaneInDirection(pub SplitDirection);
 172
 173#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 174pub struct SwapPaneInDirection(pub SplitDirection);
 175
 176#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 177pub struct MoveItemToPane {
 178    pub destination: usize,
 179    #[serde(default = "default_true")]
 180    pub focus: bool,
 181}
 182
 183#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 184pub struct MoveItemToPaneInDirection {
 185    pub direction: SplitDirection,
 186    #[serde(default = "default_true")]
 187    pub focus: bool,
 188}
 189
 190#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
 191#[serde(rename_all = "camelCase")]
 192pub struct SaveAll {
 193    pub save_intent: Option<SaveIntent>,
 194}
 195
 196#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
 197#[serde(rename_all = "camelCase")]
 198pub struct Save {
 199    pub save_intent: Option<SaveIntent>,
 200}
 201
 202#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
 203#[serde(rename_all = "camelCase")]
 204pub struct CloseAllItemsAndPanes {
 205    pub save_intent: Option<SaveIntent>,
 206}
 207
 208#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
 209#[serde(rename_all = "camelCase")]
 210pub struct CloseInactiveTabsAndPanes {
 211    pub save_intent: Option<SaveIntent>,
 212}
 213
 214#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 215pub struct SendKeystrokes(pub String);
 216
 217#[derive(Clone, Deserialize, PartialEq, Default, JsonSchema)]
 218pub struct Reload {
 219    pub binary_path: Option<PathBuf>,
 220}
 221
 222action_as!(project_symbols, ToggleProjectSymbols as Toggle);
 223
 224#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema)]
 225pub struct ToggleFileFinder {
 226    #[serde(default)]
 227    pub separate_history: bool,
 228}
 229
 230impl_action_as!(file_finder, ToggleFileFinder as Toggle);
 231
 232impl_actions!(
 233    workspace,
 234    [
 235        ActivatePane,
 236        ActivatePaneInDirection,
 237        CloseAllItemsAndPanes,
 238        CloseInactiveTabsAndPanes,
 239        MoveItemToPane,
 240        MoveItemToPaneInDirection,
 241        OpenTerminal,
 242        Reload,
 243        Save,
 244        SaveAll,
 245        SwapPaneInDirection,
 246        SendKeystrokes,
 247    ]
 248);
 249
 250#[derive(PartialEq, Eq, Debug)]
 251pub enum CloseIntent {
 252    /// Quit the program entirely.
 253    Quit,
 254    /// Close a window.
 255    CloseWindow,
 256    /// Replace the workspace in an existing window.
 257    ReplaceWindow,
 258}
 259
 260#[derive(Clone)]
 261pub struct Toast {
 262    id: NotificationId,
 263    msg: Cow<'static, str>,
 264    autohide: bool,
 265    on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
 266}
 267
 268impl Toast {
 269    pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
 270        Toast {
 271            id,
 272            msg: msg.into(),
 273            on_click: None,
 274            autohide: false,
 275        }
 276    }
 277
 278    pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
 279    where
 280        M: Into<Cow<'static, str>>,
 281        F: Fn(&mut WindowContext) + 'static,
 282    {
 283        self.on_click = Some((message.into(), Arc::new(on_click)));
 284        self
 285    }
 286
 287    pub fn autohide(mut self) -> Self {
 288        self.autohide = true;
 289        self
 290    }
 291}
 292
 293impl PartialEq for Toast {
 294    fn eq(&self, other: &Self) -> bool {
 295        self.id == other.id
 296            && self.msg == other.msg
 297            && self.on_click.is_some() == other.on_click.is_some()
 298    }
 299}
 300
 301#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema)]
 302pub struct OpenTerminal {
 303    pub working_directory: PathBuf,
 304}
 305
 306#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
 307pub struct WorkspaceId(i64);
 308
 309impl StaticColumnCount for WorkspaceId {}
 310impl Bind for WorkspaceId {
 311    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
 312        self.0.bind(statement, start_index)
 313    }
 314}
 315impl Column for WorkspaceId {
 316    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 317        i64::column(statement, start_index)
 318            .map(|(i, next_index)| (Self(i), next_index))
 319            .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
 320    }
 321}
 322impl From<WorkspaceId> for i64 {
 323    fn from(val: WorkspaceId) -> Self {
 324        val.0
 325    }
 326}
 327
 328pub fn init_settings(cx: &mut AppContext) {
 329    WorkspaceSettings::register(cx);
 330    ItemSettings::register(cx);
 331    PreviewTabsSettings::register(cx);
 332    TabBarSettings::register(cx);
 333}
 334
 335fn prompt_and_open_paths(
 336    app_state: Arc<AppState>,
 337    options: PathPromptOptions,
 338    cx: &mut AppContext,
 339) {
 340    let paths = cx.prompt_for_paths(options);
 341    cx.spawn(|cx| async move {
 342        match paths.await.anyhow().and_then(|res| res) {
 343            Ok(Some(paths)) => {
 344                cx.update(|cx| {
 345                    open_paths(&paths, app_state, OpenOptions::default(), cx).detach_and_log_err(cx)
 346                })
 347                .ok();
 348            }
 349            Ok(None) => {}
 350            Err(err) => {
 351                util::log_err(&err);
 352                cx.update(|cx| {
 353                    if let Some(workspace_window) = cx
 354                        .active_window()
 355                        .and_then(|window| window.downcast::<Workspace>())
 356                    {
 357                        workspace_window
 358                            .update(cx, |workspace, cx| {
 359                                workspace.show_portal_error(err.to_string(), cx);
 360                            })
 361                            .ok();
 362                    }
 363                })
 364                .ok();
 365            }
 366        }
 367    })
 368    .detach();
 369}
 370
 371pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
 372    init_settings(cx);
 373    notifications::init(cx);
 374    theme_preview::init(cx);
 375
 376    cx.on_action(Workspace::close_global);
 377    cx.on_action(reload);
 378
 379    cx.on_action({
 380        let app_state = Arc::downgrade(&app_state);
 381        move |_: &Open, cx: &mut AppContext| {
 382            if let Some(app_state) = app_state.upgrade() {
 383                prompt_and_open_paths(
 384                    app_state,
 385                    PathPromptOptions {
 386                        files: true,
 387                        directories: true,
 388                        multiple: true,
 389                    },
 390                    cx,
 391                );
 392            }
 393        }
 394    });
 395    cx.on_action({
 396        let app_state = Arc::downgrade(&app_state);
 397        move |_: &OpenFiles, cx: &mut AppContext| {
 398            let directories = cx.can_select_mixed_files_and_dirs();
 399            if let Some(app_state) = app_state.upgrade() {
 400                prompt_and_open_paths(
 401                    app_state,
 402                    PathPromptOptions {
 403                        files: true,
 404                        directories,
 405                        multiple: true,
 406                    },
 407                    cx,
 408                );
 409            }
 410        }
 411    });
 412}
 413
 414#[derive(Clone, Default, Deref, DerefMut)]
 415struct ProjectItemOpeners(Vec<ProjectItemOpener>);
 416
 417type ProjectItemOpener = fn(
 418    &Model<Project>,
 419    &ProjectPath,
 420    &mut WindowContext,
 421)
 422    -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
 423
 424type WorkspaceItemBuilder = Box<dyn FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
 425
 426impl Global for ProjectItemOpeners {}
 427
 428/// Registers a [ProjectItem] for the app. When opening a file, all the registered
 429/// items will get a chance to open the file, starting from the project item that
 430/// was added last.
 431pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
 432    let builders = cx.default_global::<ProjectItemOpeners>();
 433    builders.push(|project, project_path, cx| {
 434        let project_item = <I::Item as project::ProjectItem>::try_open(project, project_path, cx)?;
 435        let project = project.clone();
 436        Some(cx.spawn(|cx| async move {
 437            let project_item = project_item.await?;
 438            let project_entry_id: Option<ProjectEntryId> =
 439                project_item.read_with(&cx, project::ProjectItem::entry_id)?;
 440            let build_workspace_item = Box::new(|cx: &mut ViewContext<Pane>| {
 441                Box::new(cx.new_view(|cx| I::for_project_item(project, project_item, cx)))
 442                    as Box<dyn ItemHandle>
 443            }) as Box<_>;
 444            Ok((project_entry_id, build_workspace_item))
 445        }))
 446    });
 447}
 448
 449#[derive(Default)]
 450pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
 451
 452struct FollowableViewDescriptor {
 453    from_state_proto: fn(
 454        View<Workspace>,
 455        ViewId,
 456        &mut Option<proto::view::Variant>,
 457        &mut WindowContext,
 458    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
 459    to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
 460}
 461
 462impl Global for FollowableViewRegistry {}
 463
 464impl FollowableViewRegistry {
 465    pub fn register<I: FollowableItem>(cx: &mut AppContext) {
 466        cx.default_global::<Self>().0.insert(
 467            TypeId::of::<I>(),
 468            FollowableViewDescriptor {
 469                from_state_proto: |workspace, id, state, cx| {
 470                    I::from_state_proto(workspace, id, state, cx).map(|task| {
 471                        cx.foreground_executor()
 472                            .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
 473                    })
 474                },
 475                to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
 476            },
 477        );
 478    }
 479
 480    pub fn from_state_proto(
 481        workspace: View<Workspace>,
 482        view_id: ViewId,
 483        mut state: Option<proto::view::Variant>,
 484        cx: &mut WindowContext,
 485    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
 486        cx.update_default_global(|this: &mut Self, cx| {
 487            this.0.values().find_map(|descriptor| {
 488                (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, cx)
 489            })
 490        })
 491    }
 492
 493    pub fn to_followable_view(
 494        view: impl Into<AnyView>,
 495        cx: &AppContext,
 496    ) -> Option<Box<dyn FollowableItemHandle>> {
 497        let this = cx.try_global::<Self>()?;
 498        let view = view.into();
 499        let descriptor = this.0.get(&view.entity_type())?;
 500        Some((descriptor.to_followable_view)(&view))
 501    }
 502}
 503
 504#[derive(Copy, Clone)]
 505struct SerializableItemDescriptor {
 506    deserialize: fn(
 507        Model<Project>,
 508        WeakView<Workspace>,
 509        WorkspaceId,
 510        ItemId,
 511        &mut ViewContext<Pane>,
 512    ) -> Task<Result<Box<dyn ItemHandle>>>,
 513    cleanup: fn(WorkspaceId, Vec<ItemId>, &mut WindowContext) -> Task<Result<()>>,
 514    view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
 515}
 516
 517#[derive(Default)]
 518struct SerializableItemRegistry {
 519    descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
 520    descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
 521}
 522
 523impl Global for SerializableItemRegistry {}
 524
 525impl SerializableItemRegistry {
 526    fn deserialize(
 527        item_kind: &str,
 528        project: Model<Project>,
 529        workspace: WeakView<Workspace>,
 530        workspace_id: WorkspaceId,
 531        item_item: ItemId,
 532        cx: &mut ViewContext<Pane>,
 533    ) -> Task<Result<Box<dyn ItemHandle>>> {
 534        let Some(descriptor) = Self::descriptor(item_kind, cx) else {
 535            return Task::ready(Err(anyhow!(
 536                "cannot deserialize {}, descriptor not found",
 537                item_kind
 538            )));
 539        };
 540
 541        (descriptor.deserialize)(project, workspace, workspace_id, item_item, cx)
 542    }
 543
 544    fn cleanup(
 545        item_kind: &str,
 546        workspace_id: WorkspaceId,
 547        loaded_items: Vec<ItemId>,
 548        cx: &mut WindowContext,
 549    ) -> Task<Result<()>> {
 550        let Some(descriptor) = Self::descriptor(item_kind, cx) else {
 551            return Task::ready(Err(anyhow!(
 552                "cannot cleanup {}, descriptor not found",
 553                item_kind
 554            )));
 555        };
 556
 557        (descriptor.cleanup)(workspace_id, loaded_items, cx)
 558    }
 559
 560    fn view_to_serializable_item_handle(
 561        view: AnyView,
 562        cx: &AppContext,
 563    ) -> Option<Box<dyn SerializableItemHandle>> {
 564        let this = cx.try_global::<Self>()?;
 565        let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
 566        Some((descriptor.view_to_serializable_item)(view))
 567    }
 568
 569    fn descriptor(item_kind: &str, cx: &AppContext) -> Option<SerializableItemDescriptor> {
 570        let this = cx.try_global::<Self>()?;
 571        this.descriptors_by_kind.get(item_kind).copied()
 572    }
 573}
 574
 575pub fn register_serializable_item<I: SerializableItem>(cx: &mut AppContext) {
 576    let serialized_item_kind = I::serialized_item_kind();
 577
 578    let registry = cx.default_global::<SerializableItemRegistry>();
 579    let descriptor = SerializableItemDescriptor {
 580        deserialize: |project, workspace, workspace_id, item_id, cx| {
 581            let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
 582            cx.foreground_executor()
 583                .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
 584        },
 585        cleanup: |workspace_id, loaded_items, cx| I::cleanup(workspace_id, loaded_items, cx),
 586        view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
 587    };
 588    registry
 589        .descriptors_by_kind
 590        .insert(Arc::from(serialized_item_kind), descriptor);
 591    registry
 592        .descriptors_by_type
 593        .insert(TypeId::of::<I>(), descriptor);
 594}
 595
 596pub struct AppState {
 597    pub languages: Arc<LanguageRegistry>,
 598    pub client: Arc<Client>,
 599    pub user_store: Model<UserStore>,
 600    pub workspace_store: Model<WorkspaceStore>,
 601    pub fs: Arc<dyn fs::Fs>,
 602    pub build_window_options: fn(Option<Uuid>, &mut AppContext) -> WindowOptions,
 603    pub node_runtime: NodeRuntime,
 604    pub session: Model<AppSession>,
 605}
 606
 607struct GlobalAppState(Weak<AppState>);
 608
 609impl Global for GlobalAppState {}
 610
 611pub struct WorkspaceStore {
 612    workspaces: HashSet<WindowHandle<Workspace>>,
 613    client: Arc<Client>,
 614    _subscriptions: Vec<client::Subscription>,
 615}
 616
 617#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
 618struct Follower {
 619    project_id: Option<u64>,
 620    peer_id: PeerId,
 621}
 622
 623impl AppState {
 624    pub fn global(cx: &AppContext) -> Weak<Self> {
 625        cx.global::<GlobalAppState>().0.clone()
 626    }
 627    pub fn try_global(cx: &AppContext) -> Option<Weak<Self>> {
 628        cx.try_global::<GlobalAppState>()
 629            .map(|state| state.0.clone())
 630    }
 631    pub fn set_global(state: Weak<AppState>, cx: &mut AppContext) {
 632        cx.set_global(GlobalAppState(state));
 633    }
 634
 635    #[cfg(any(test, feature = "test-support"))]
 636    pub fn test(cx: &mut AppContext) -> Arc<Self> {
 637        use node_runtime::NodeRuntime;
 638        use session::Session;
 639        use settings::SettingsStore;
 640
 641        if !cx.has_global::<SettingsStore>() {
 642            let settings_store = SettingsStore::test(cx);
 643            cx.set_global(settings_store);
 644        }
 645
 646        let fs = fs::FakeFs::new(cx.background_executor().clone());
 647        let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
 648        let clock = Arc::new(clock::FakeSystemClock::new());
 649        let http_client = http_client::FakeHttpClient::with_404_response();
 650        let client = Client::new(clock, http_client.clone(), cx);
 651        let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
 652        let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
 653        let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
 654
 655        theme::init(theme::LoadThemes::JustBase, cx);
 656        client::init(&client, cx);
 657        crate::init_settings(cx);
 658
 659        Arc::new(Self {
 660            client,
 661            fs,
 662            languages,
 663            user_store,
 664            workspace_store,
 665            node_runtime: NodeRuntime::unavailable(),
 666            build_window_options: |_, _| Default::default(),
 667            session,
 668        })
 669    }
 670}
 671
 672struct DelayedDebouncedEditAction {
 673    task: Option<Task<()>>,
 674    cancel_channel: Option<oneshot::Sender<()>>,
 675}
 676
 677impl DelayedDebouncedEditAction {
 678    fn new() -> DelayedDebouncedEditAction {
 679        DelayedDebouncedEditAction {
 680            task: None,
 681            cancel_channel: None,
 682        }
 683    }
 684
 685    fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
 686    where
 687        F: 'static + Send + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
 688    {
 689        if let Some(channel) = self.cancel_channel.take() {
 690            _ = channel.send(());
 691        }
 692
 693        let (sender, mut receiver) = oneshot::channel::<()>();
 694        self.cancel_channel = Some(sender);
 695
 696        let previous_task = self.task.take();
 697        self.task = Some(cx.spawn(move |workspace, mut cx| async move {
 698            let mut timer = cx.background_executor().timer(delay).fuse();
 699            if let Some(previous_task) = previous_task {
 700                previous_task.await;
 701            }
 702
 703            futures::select_biased! {
 704                _ = receiver => return,
 705                    _ = timer => {}
 706            }
 707
 708            if let Some(result) = workspace
 709                .update(&mut cx, |workspace, cx| (func)(workspace, cx))
 710                .log_err()
 711            {
 712                result.await.log_err();
 713            }
 714        }));
 715    }
 716}
 717
 718pub enum Event {
 719    PaneAdded(View<Pane>),
 720    PaneRemoved,
 721    ItemAdded {
 722        item: Box<dyn ItemHandle>,
 723    },
 724    ItemRemoved,
 725    ActiveItemChanged,
 726    UserSavedItem {
 727        pane: WeakView<Pane>,
 728        item: Box<dyn WeakItemHandle>,
 729        save_intent: SaveIntent,
 730    },
 731    ContactRequestedJoin(u64),
 732    WorkspaceCreated(WeakView<Workspace>),
 733    SpawnTask {
 734        action: Box<SpawnInTerminal>,
 735    },
 736    OpenBundledFile {
 737        text: Cow<'static, str>,
 738        title: &'static str,
 739        language: &'static str,
 740    },
 741    ZoomChanged,
 742}
 743
 744#[derive(Debug)]
 745pub enum OpenVisible {
 746    All,
 747    None,
 748    OnlyFiles,
 749    OnlyDirectories,
 750}
 751
 752type PromptForNewPath = Box<
 753    dyn Fn(&mut Workspace, &mut ViewContext<Workspace>) -> oneshot::Receiver<Option<ProjectPath>>,
 754>;
 755
 756type PromptForOpenPath = Box<
 757    dyn Fn(
 758        &mut Workspace,
 759        DirectoryLister,
 760        &mut ViewContext<Workspace>,
 761    ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
 762>;
 763
 764/// Collects everything project-related for a certain window opened.
 765/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
 766///
 767/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
 768/// The `Workspace` owns everybody's state and serves as a default, "global context",
 769/// that can be used to register a global action to be triggered from any place in the window.
 770pub struct Workspace {
 771    weak_self: WeakView<Self>,
 772    workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
 773    zoomed: Option<AnyWeakView>,
 774    previous_dock_drag_coordinates: Option<Point<Pixels>>,
 775    zoomed_position: Option<DockPosition>,
 776    center: PaneGroup,
 777    left_dock: View<Dock>,
 778    bottom_dock: View<Dock>,
 779    right_dock: View<Dock>,
 780    panes: Vec<View<Pane>>,
 781    panes_by_item: HashMap<EntityId, WeakView<Pane>>,
 782    active_pane: View<Pane>,
 783    last_active_center_pane: Option<WeakView<Pane>>,
 784    last_active_view_id: Option<proto::ViewId>,
 785    status_bar: View<StatusBar>,
 786    modal_layer: View<ModalLayer>,
 787    titlebar_item: Option<AnyView>,
 788    notifications: Vec<(NotificationId, Box<dyn NotificationHandle>)>,
 789    project: Model<Project>,
 790    follower_states: HashMap<PeerId, FollowerState>,
 791    last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
 792    window_edited: bool,
 793    active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
 794    leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
 795    database_id: Option<WorkspaceId>,
 796    app_state: Arc<AppState>,
 797    dispatching_keystrokes: Rc<RefCell<(HashSet<String>, Vec<Keystroke>)>>,
 798    _subscriptions: Vec<Subscription>,
 799    _apply_leader_updates: Task<Result<()>>,
 800    _observe_current_user: Task<Result<()>>,
 801    _schedule_serialize: Option<Task<()>>,
 802    pane_history_timestamp: Arc<AtomicUsize>,
 803    bounds: Bounds<Pixels>,
 804    centered_layout: bool,
 805    bounds_save_task_queued: Option<Task<()>>,
 806    on_prompt_for_new_path: Option<PromptForNewPath>,
 807    on_prompt_for_open_path: Option<PromptForOpenPath>,
 808    serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
 809    serialized_ssh_project: Option<SerializedSshProject>,
 810    _items_serializer: Task<Result<()>>,
 811    session_id: Option<String>,
 812}
 813
 814impl EventEmitter<Event> for Workspace {}
 815
 816#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 817pub struct ViewId {
 818    pub creator: PeerId,
 819    pub id: u64,
 820}
 821
 822pub struct FollowerState {
 823    center_pane: View<Pane>,
 824    dock_pane: Option<View<Pane>>,
 825    active_view_id: Option<ViewId>,
 826    items_by_leader_view_id: HashMap<ViewId, FollowerView>,
 827}
 828
 829struct FollowerView {
 830    view: Box<dyn FollowableItemHandle>,
 831    location: Option<proto::PanelId>,
 832}
 833
 834impl Workspace {
 835    const DEFAULT_PADDING: f32 = 0.2;
 836    const MAX_PADDING: f32 = 0.4;
 837
 838    pub fn new(
 839        workspace_id: Option<WorkspaceId>,
 840        project: Model<Project>,
 841        app_state: Arc<AppState>,
 842        cx: &mut ViewContext<Self>,
 843    ) -> Self {
 844        cx.observe(&project, |_, _, cx| cx.notify()).detach();
 845        cx.subscribe(&project, move |this, _, event, cx| {
 846            match event {
 847                project::Event::RemoteIdChanged(_) => {
 848                    this.update_window_title(cx);
 849                }
 850
 851                project::Event::CollaboratorLeft(peer_id) => {
 852                    this.collaborator_left(*peer_id, cx);
 853                }
 854
 855                project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
 856                    this.update_window_title(cx);
 857                    this.serialize_workspace(cx);
 858                }
 859
 860                project::Event::DisconnectedFromHost => {
 861                    this.update_window_edited(cx);
 862                    let leaders_to_unfollow =
 863                        this.follower_states.keys().copied().collect::<Vec<_>>();
 864                    for leader_id in leaders_to_unfollow {
 865                        this.unfollow(leader_id, cx);
 866                    }
 867                }
 868
 869                project::Event::DisconnectedFromSshRemote => {
 870                    this.update_window_edited(cx);
 871                }
 872
 873                project::Event::Closed => {
 874                    cx.remove_window();
 875                }
 876
 877                project::Event::DeletedEntry(_, entry_id) => {
 878                    for pane in this.panes.iter() {
 879                        pane.update(cx, |pane, cx| {
 880                            pane.handle_deleted_project_item(*entry_id, cx)
 881                        });
 882                    }
 883                }
 884
 885                project::Event::Toast {
 886                    notification_id,
 887                    message,
 888                } => this.show_notification(
 889                    NotificationId::named(notification_id.clone()),
 890                    cx,
 891                    |cx| cx.new_view(|_| MessageNotification::new(message.clone())),
 892                ),
 893
 894                project::Event::HideToast { notification_id } => {
 895                    this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
 896                }
 897
 898                project::Event::LanguageServerPrompt(request) => {
 899                    struct LanguageServerPrompt;
 900
 901                    let mut hasher = DefaultHasher::new();
 902                    request.lsp_name.as_str().hash(&mut hasher);
 903                    let id = hasher.finish();
 904
 905                    this.show_notification(
 906                        NotificationId::composite::<LanguageServerPrompt>(id as usize),
 907                        cx,
 908                        |cx| {
 909                            cx.new_view(|_| {
 910                                notifications::LanguageServerPrompt::new(request.clone())
 911                            })
 912                        },
 913                    );
 914                }
 915
 916                _ => {}
 917            }
 918            cx.notify()
 919        })
 920        .detach();
 921
 922        cx.on_focus_lost(|this, cx| {
 923            let focus_handle = this.focus_handle(cx);
 924            cx.focus(&focus_handle);
 925        })
 926        .detach();
 927
 928        let weak_handle = cx.view().downgrade();
 929        let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
 930
 931        let center_pane = cx.new_view(|cx| {
 932            let mut center_pane = Pane::new(
 933                weak_handle.clone(),
 934                project.clone(),
 935                pane_history_timestamp.clone(),
 936                None,
 937                NewFile.boxed_clone(),
 938                cx,
 939            );
 940            center_pane.set_can_split(Some(Arc::new(|_, _, _| true)));
 941            center_pane
 942        });
 943        cx.subscribe(&center_pane, Self::handle_pane_event).detach();
 944
 945        cx.focus_view(&center_pane);
 946        cx.emit(Event::PaneAdded(center_pane.clone()));
 947
 948        let window_handle = cx.window_handle().downcast::<Workspace>().unwrap();
 949        app_state.workspace_store.update(cx, |store, _| {
 950            store.workspaces.insert(window_handle);
 951        });
 952
 953        let mut current_user = app_state.user_store.read(cx).watch_current_user();
 954        let mut connection_status = app_state.client.status();
 955        let _observe_current_user = cx.spawn(|this, mut cx| async move {
 956            current_user.next().await;
 957            connection_status.next().await;
 958            let mut stream =
 959                Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
 960
 961            while stream.recv().await.is_some() {
 962                this.update(&mut cx, |_, cx| cx.notify())?;
 963            }
 964            anyhow::Ok(())
 965        });
 966
 967        // All leader updates are enqueued and then processed in a single task, so
 968        // that each asynchronous operation can be run in order.
 969        let (leader_updates_tx, mut leader_updates_rx) =
 970            mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
 971        let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
 972            while let Some((leader_id, update)) = leader_updates_rx.next().await {
 973                Self::process_leader_update(&this, leader_id, update, &mut cx)
 974                    .await
 975                    .log_err();
 976            }
 977
 978            Ok(())
 979        });
 980
 981        cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
 982
 983        let left_dock = Dock::new(DockPosition::Left, cx);
 984        let bottom_dock = Dock::new(DockPosition::Bottom, cx);
 985        let right_dock = Dock::new(DockPosition::Right, cx);
 986        let left_dock_buttons = cx.new_view(|cx| PanelButtons::new(left_dock.clone(), cx));
 987        let bottom_dock_buttons = cx.new_view(|cx| PanelButtons::new(bottom_dock.clone(), cx));
 988        let right_dock_buttons = cx.new_view(|cx| PanelButtons::new(right_dock.clone(), cx));
 989        let status_bar = cx.new_view(|cx| {
 990            let mut status_bar = StatusBar::new(&center_pane.clone(), cx);
 991            status_bar.add_left_item(left_dock_buttons, cx);
 992            status_bar.add_right_item(right_dock_buttons, cx);
 993            status_bar.add_right_item(bottom_dock_buttons, cx);
 994            status_bar
 995        });
 996
 997        let modal_layer = cx.new_view(|_| ModalLayer::new());
 998
 999        let session_id = app_state.session.read(cx).id().to_owned();
1000
1001        let mut active_call = None;
1002        if let Some(call) = ActiveCall::try_global(cx) {
1003            let call = call.clone();
1004            let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
1005            active_call = Some((call, subscriptions));
1006        }
1007
1008        let (serializable_items_tx, serializable_items_rx) =
1009            mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1010        let _items_serializer = cx.spawn(|this, mut cx| async move {
1011            Self::serialize_items(&this, serializable_items_rx, &mut cx).await
1012        });
1013
1014        let subscriptions = vec![
1015            cx.observe_window_activation(Self::on_window_activation_changed),
1016            cx.observe_window_bounds(move |this, cx| {
1017                if this.bounds_save_task_queued.is_some() {
1018                    return;
1019                }
1020                this.bounds_save_task_queued = Some(cx.spawn(|this, mut cx| async move {
1021                    cx.background_executor()
1022                        .timer(Duration::from_millis(100))
1023                        .await;
1024                    this.update(&mut cx, |this, cx| {
1025                        if let Some(display) = cx.display() {
1026                            if let Ok(display_uuid) = display.uuid() {
1027                                let window_bounds = cx.inner_window_bounds();
1028                                if let Some(database_id) = workspace_id {
1029                                    cx.background_executor()
1030                                        .spawn(DB.set_window_open_status(
1031                                            database_id,
1032                                            SerializedWindowBounds(window_bounds),
1033                                            display_uuid,
1034                                        ))
1035                                        .detach_and_log_err(cx);
1036                                }
1037                            }
1038                        }
1039                        this.bounds_save_task_queued.take();
1040                    })
1041                    .ok();
1042                }));
1043                cx.notify();
1044            }),
1045            cx.observe_window_appearance(|_, cx| {
1046                let window_appearance = cx.appearance();
1047
1048                *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1049
1050                ThemeSettings::reload_current_theme(cx);
1051            }),
1052            cx.on_release(|this, window, cx| {
1053                this.app_state.workspace_store.update(cx, |store, _| {
1054                    let window = window.downcast::<Self>().unwrap();
1055                    store.workspaces.remove(&window);
1056                })
1057            }),
1058        ];
1059
1060        cx.defer(|this, cx| {
1061            this.update_window_title(cx);
1062        });
1063        Workspace {
1064            weak_self: weak_handle.clone(),
1065            zoomed: None,
1066            zoomed_position: None,
1067            previous_dock_drag_coordinates: None,
1068            center: PaneGroup::new(center_pane.clone()),
1069            panes: vec![center_pane.clone()],
1070            panes_by_item: Default::default(),
1071            active_pane: center_pane.clone(),
1072            last_active_center_pane: Some(center_pane.downgrade()),
1073            last_active_view_id: None,
1074            status_bar,
1075            modal_layer,
1076            titlebar_item: None,
1077            notifications: Default::default(),
1078            left_dock,
1079            bottom_dock,
1080            right_dock,
1081            project: project.clone(),
1082            follower_states: Default::default(),
1083            last_leaders_by_pane: Default::default(),
1084            dispatching_keystrokes: Default::default(),
1085            window_edited: false,
1086            active_call,
1087            database_id: workspace_id,
1088            app_state,
1089            _observe_current_user,
1090            _apply_leader_updates,
1091            _schedule_serialize: None,
1092            leader_updates_tx,
1093            _subscriptions: subscriptions,
1094            pane_history_timestamp,
1095            workspace_actions: Default::default(),
1096            // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1097            bounds: Default::default(),
1098            centered_layout: false,
1099            bounds_save_task_queued: None,
1100            on_prompt_for_new_path: None,
1101            on_prompt_for_open_path: None,
1102            serializable_items_tx,
1103            _items_serializer,
1104            session_id: Some(session_id),
1105            serialized_ssh_project: None,
1106        }
1107    }
1108
1109    pub fn new_local(
1110        abs_paths: Vec<PathBuf>,
1111        app_state: Arc<AppState>,
1112        requesting_window: Option<WindowHandle<Workspace>>,
1113        env: Option<HashMap<String, String>>,
1114        cx: &mut AppContext,
1115    ) -> Task<
1116        anyhow::Result<(
1117            WindowHandle<Workspace>,
1118            Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
1119        )>,
1120    > {
1121        let project_handle = Project::local(
1122            app_state.client.clone(),
1123            app_state.node_runtime.clone(),
1124            app_state.user_store.clone(),
1125            app_state.languages.clone(),
1126            app_state.fs.clone(),
1127            env,
1128            cx,
1129        );
1130
1131        cx.spawn(|mut cx| async move {
1132            let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1133            for path in abs_paths.into_iter() {
1134                if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1135                    paths_to_open.push(canonical)
1136                } else {
1137                    paths_to_open.push(path)
1138                }
1139            }
1140
1141            let serialized_workspace: Option<SerializedWorkspace> =
1142                persistence::DB.workspace_for_roots(paths_to_open.as_slice());
1143
1144            let workspace_location = serialized_workspace
1145                .as_ref()
1146                .map(|ws| &ws.location)
1147                .and_then(|loc| match loc {
1148                    SerializedWorkspaceLocation::Local(_, order) => {
1149                        Some((loc.sorted_paths(), order.order()))
1150                    }
1151                    _ => None,
1152                });
1153
1154            if let Some((paths, order)) = workspace_location {
1155                paths_to_open = paths.iter().cloned().collect();
1156
1157                if order.iter().enumerate().any(|(i, &j)| i != j) {
1158                    project_handle
1159                        .update(&mut cx, |project, cx| {
1160                            project.set_worktrees_reordered(true, cx);
1161                        })
1162                        .log_err();
1163                }
1164            }
1165
1166            // Get project paths for all of the abs_paths
1167            let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1168                Vec::with_capacity(paths_to_open.len());
1169            for path in paths_to_open.into_iter() {
1170                if let Some((_, project_entry)) = cx
1171                    .update(|cx| {
1172                        Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1173                    })?
1174                    .await
1175                    .log_err()
1176                {
1177                    project_paths.push((path, Some(project_entry)));
1178                } else {
1179                    project_paths.push((path, None));
1180                }
1181            }
1182
1183            let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1184                serialized_workspace.id
1185            } else {
1186                DB.next_id().await.unwrap_or_else(|_| Default::default())
1187            };
1188
1189            let toolchains = DB.toolchains(workspace_id).await?;
1190            for (toolchain, worktree_id) in toolchains {
1191                project_handle
1192                    .update(&mut cx, |this, cx| {
1193                        this.activate_toolchain(worktree_id, toolchain, cx)
1194                    })?
1195                    .await;
1196            }
1197            let window = if let Some(window) = requesting_window {
1198                cx.update_window(window.into(), |_, cx| {
1199                    cx.replace_root_view(|cx| {
1200                        Workspace::new(
1201                            Some(workspace_id),
1202                            project_handle.clone(),
1203                            app_state.clone(),
1204                            cx,
1205                        )
1206                    });
1207                })?;
1208                window
1209            } else {
1210                let window_bounds_override = window_bounds_env_override();
1211
1212                let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1213                    (Some(WindowBounds::Windowed(bounds)), None)
1214                } else {
1215                    let restorable_bounds = serialized_workspace
1216                        .as_ref()
1217                        .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
1218                        .or_else(|| {
1219                            let (display, window_bounds) = DB.last_window().log_err()?;
1220                            Some((display?, window_bounds?))
1221                        });
1222
1223                    if let Some((serialized_display, serialized_status)) = restorable_bounds {
1224                        (Some(serialized_status.0), Some(serialized_display))
1225                    } else {
1226                        (None, None)
1227                    }
1228                };
1229
1230                // Use the serialized workspace to construct the new window
1231                let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?;
1232                options.window_bounds = window_bounds;
1233                let centered_layout = serialized_workspace
1234                    .as_ref()
1235                    .map(|w| w.centered_layout)
1236                    .unwrap_or(false);
1237                cx.open_window(options, {
1238                    let app_state = app_state.clone();
1239                    let project_handle = project_handle.clone();
1240                    move |cx| {
1241                        cx.new_view(|cx| {
1242                            let mut workspace =
1243                                Workspace::new(Some(workspace_id), project_handle, app_state, cx);
1244                            workspace.centered_layout = centered_layout;
1245                            workspace
1246                        })
1247                    }
1248                })?
1249            };
1250
1251            notify_if_database_failed(window, &mut cx);
1252            let opened_items = window
1253                .update(&mut cx, |_workspace, cx| {
1254                    open_items(serialized_workspace, project_paths, cx)
1255                })?
1256                .await
1257                .unwrap_or_default();
1258
1259            window
1260                .update(&mut cx, |_, cx| cx.activate_window())
1261                .log_err();
1262            Ok((window, opened_items))
1263        })
1264    }
1265
1266    pub fn weak_handle(&self) -> WeakView<Self> {
1267        self.weak_self.clone()
1268    }
1269
1270    pub fn left_dock(&self) -> &View<Dock> {
1271        &self.left_dock
1272    }
1273
1274    pub fn bottom_dock(&self) -> &View<Dock> {
1275        &self.bottom_dock
1276    }
1277
1278    pub fn right_dock(&self) -> &View<Dock> {
1279        &self.right_dock
1280    }
1281
1282    pub fn is_edited(&self) -> bool {
1283        self.window_edited
1284    }
1285
1286    pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
1287        let focus_handle = panel.focus_handle(cx);
1288        cx.on_focus_in(&focus_handle, Self::handle_panel_focused)
1289            .detach();
1290
1291        let dock = match panel.position(cx) {
1292            DockPosition::Left => &self.left_dock,
1293            DockPosition::Bottom => &self.bottom_dock,
1294            DockPosition::Right => &self.right_dock,
1295        };
1296
1297        dock.update(cx, |dock, cx| {
1298            dock.add_panel(panel, self.weak_self.clone(), cx)
1299        });
1300    }
1301
1302    pub fn status_bar(&self) -> &View<StatusBar> {
1303        &self.status_bar
1304    }
1305
1306    pub fn app_state(&self) -> &Arc<AppState> {
1307        &self.app_state
1308    }
1309
1310    pub fn user_store(&self) -> &Model<UserStore> {
1311        &self.app_state.user_store
1312    }
1313
1314    pub fn project(&self) -> &Model<Project> {
1315        &self.project
1316    }
1317
1318    pub fn recent_navigation_history_iter(
1319        &self,
1320        cx: &AppContext,
1321    ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> {
1322        let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
1323        let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
1324        for pane in &self.panes {
1325            let pane = pane.read(cx);
1326            pane.nav_history()
1327                .for_each_entry(cx, |entry, (project_path, fs_path)| {
1328                    if let Some(fs_path) = &fs_path {
1329                        abs_paths_opened
1330                            .entry(fs_path.clone())
1331                            .or_default()
1332                            .insert(project_path.clone());
1333                    }
1334                    let timestamp = entry.timestamp;
1335                    match history.entry(project_path) {
1336                        hash_map::Entry::Occupied(mut entry) => {
1337                            let (_, old_timestamp) = entry.get();
1338                            if &timestamp > old_timestamp {
1339                                entry.insert((fs_path, timestamp));
1340                            }
1341                        }
1342                        hash_map::Entry::Vacant(entry) => {
1343                            entry.insert((fs_path, timestamp));
1344                        }
1345                    }
1346                });
1347        }
1348
1349        history
1350            .into_iter()
1351            .sorted_by_key(|(_, (_, timestamp))| *timestamp)
1352            .map(|(project_path, (fs_path, _))| (project_path, fs_path))
1353            .rev()
1354            .filter(move |(history_path, abs_path)| {
1355                let latest_project_path_opened = abs_path
1356                    .as_ref()
1357                    .and_then(|abs_path| abs_paths_opened.get(abs_path))
1358                    .and_then(|project_paths| {
1359                        project_paths
1360                            .iter()
1361                            .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
1362                    });
1363
1364                match latest_project_path_opened {
1365                    Some(latest_project_path_opened) => latest_project_path_opened == history_path,
1366                    None => true,
1367                }
1368            })
1369    }
1370
1371    pub fn recent_navigation_history(
1372        &self,
1373        limit: Option<usize>,
1374        cx: &AppContext,
1375    ) -> Vec<(ProjectPath, Option<PathBuf>)> {
1376        self.recent_navigation_history_iter(cx)
1377            .take(limit.unwrap_or(usize::MAX))
1378            .collect()
1379    }
1380
1381    fn navigate_history(
1382        &mut self,
1383        pane: WeakView<Pane>,
1384        mode: NavigationMode,
1385        cx: &mut ViewContext<Workspace>,
1386    ) -> Task<Result<()>> {
1387        let to_load = if let Some(pane) = pane.upgrade() {
1388            pane.update(cx, |pane, cx| {
1389                pane.focus(cx);
1390                loop {
1391                    // Retrieve the weak item handle from the history.
1392                    let entry = pane.nav_history_mut().pop(mode, cx)?;
1393
1394                    // If the item is still present in this pane, then activate it.
1395                    if let Some(index) = entry
1396                        .item
1397                        .upgrade()
1398                        .and_then(|v| pane.index_for_item(v.as_ref()))
1399                    {
1400                        let prev_active_item_index = pane.active_item_index();
1401                        pane.nav_history_mut().set_mode(mode);
1402                        pane.activate_item(index, true, true, cx);
1403                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
1404
1405                        let mut navigated = prev_active_item_index != pane.active_item_index();
1406                        if let Some(data) = entry.data {
1407                            navigated |= pane.active_item()?.navigate(data, cx);
1408                        }
1409
1410                        if navigated {
1411                            break None;
1412                        }
1413                    } else {
1414                        // If the item is no longer present in this pane, then retrieve its
1415                        // path info in order to reopen it.
1416                        break pane
1417                            .nav_history()
1418                            .path_for_item(entry.item.id())
1419                            .map(|(project_path, abs_path)| (project_path, abs_path, entry));
1420                    }
1421                }
1422            })
1423        } else {
1424            None
1425        };
1426
1427        if let Some((project_path, abs_path, entry)) = to_load {
1428            // If the item was no longer present, then load it again from its previous path, first try the local path
1429            let open_by_project_path = self.load_path(project_path.clone(), cx);
1430
1431            cx.spawn(|workspace, mut cx| async move {
1432                let open_by_project_path = open_by_project_path.await;
1433                let mut navigated = false;
1434                match open_by_project_path
1435                    .with_context(|| format!("Navigating to {project_path:?}"))
1436                {
1437                    Ok((project_entry_id, build_item)) => {
1438                        let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1439                            pane.nav_history_mut().set_mode(mode);
1440                            pane.active_item().map(|p| p.item_id())
1441                        })?;
1442
1443                        pane.update(&mut cx, |pane, cx| {
1444                            let item = pane.open_item(
1445                                project_entry_id,
1446                                true,
1447                                entry.is_preview,
1448                                None,
1449                                cx,
1450                                build_item,
1451                            );
1452                            navigated |= Some(item.item_id()) != prev_active_item_id;
1453                            pane.nav_history_mut().set_mode(NavigationMode::Normal);
1454                            if let Some(data) = entry.data {
1455                                navigated |= item.navigate(data, cx);
1456                            }
1457                        })?;
1458                    }
1459                    Err(open_by_project_path_e) => {
1460                        // Fall back to opening by abs path, in case an external file was opened and closed,
1461                        // and its worktree is now dropped
1462                        if let Some(abs_path) = abs_path {
1463                            let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1464                                pane.nav_history_mut().set_mode(mode);
1465                                pane.active_item().map(|p| p.item_id())
1466                            })?;
1467                            let open_by_abs_path = workspace.update(&mut cx, |workspace, cx| {
1468                                workspace.open_abs_path(abs_path.clone(), false, cx)
1469                            })?;
1470                            match open_by_abs_path
1471                                .await
1472                                .with_context(|| format!("Navigating to {abs_path:?}"))
1473                            {
1474                                Ok(item) => {
1475                                    pane.update(&mut cx, |pane, cx| {
1476                                        navigated |= Some(item.item_id()) != prev_active_item_id;
1477                                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
1478                                        if let Some(data) = entry.data {
1479                                            navigated |= item.navigate(data, cx);
1480                                        }
1481                                    })?;
1482                                }
1483                                Err(open_by_abs_path_e) => {
1484                                    log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
1485                                }
1486                            }
1487                        }
1488                    }
1489                }
1490
1491                if !navigated {
1492                    workspace
1493                        .update(&mut cx, |workspace, cx| {
1494                            Self::navigate_history(workspace, pane, mode, cx)
1495                        })?
1496                        .await?;
1497                }
1498
1499                Ok(())
1500            })
1501        } else {
1502            Task::ready(Ok(()))
1503        }
1504    }
1505
1506    pub fn go_back(
1507        &mut self,
1508        pane: WeakView<Pane>,
1509        cx: &mut ViewContext<Workspace>,
1510    ) -> Task<Result<()>> {
1511        self.navigate_history(pane, NavigationMode::GoingBack, cx)
1512    }
1513
1514    pub fn go_forward(
1515        &mut self,
1516        pane: WeakView<Pane>,
1517        cx: &mut ViewContext<Workspace>,
1518    ) -> Task<Result<()>> {
1519        self.navigate_history(pane, NavigationMode::GoingForward, cx)
1520    }
1521
1522    pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
1523        self.navigate_history(
1524            self.active_pane().downgrade(),
1525            NavigationMode::ReopeningClosedItem,
1526            cx,
1527        )
1528    }
1529
1530    pub fn client(&self) -> &Arc<Client> {
1531        &self.app_state.client
1532    }
1533
1534    pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
1535        self.titlebar_item = Some(item);
1536        cx.notify();
1537    }
1538
1539    pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
1540        self.on_prompt_for_new_path = Some(prompt)
1541    }
1542
1543    pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
1544        self.on_prompt_for_open_path = Some(prompt)
1545    }
1546
1547    pub fn serialized_ssh_project(&self) -> Option<SerializedSshProject> {
1548        self.serialized_ssh_project.clone()
1549    }
1550
1551    pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) {
1552        self.serialized_ssh_project = Some(serialized_ssh_project);
1553    }
1554
1555    pub fn prompt_for_open_path(
1556        &mut self,
1557        path_prompt_options: PathPromptOptions,
1558        lister: DirectoryLister,
1559        cx: &mut ViewContext<Self>,
1560    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
1561        if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
1562            let prompt = self.on_prompt_for_open_path.take().unwrap();
1563            let rx = prompt(self, lister, cx);
1564            self.on_prompt_for_open_path = Some(prompt);
1565            rx
1566        } else {
1567            let (tx, rx) = oneshot::channel();
1568            let abs_path = cx.prompt_for_paths(path_prompt_options);
1569
1570            cx.spawn(|this, mut cx| async move {
1571                let Ok(result) = abs_path.await else {
1572                    return Ok(());
1573                };
1574
1575                match result {
1576                    Ok(result) => {
1577                        tx.send(result).log_err();
1578                    }
1579                    Err(err) => {
1580                        let rx = this.update(&mut cx, |this, cx| {
1581                            this.show_portal_error(err.to_string(), cx);
1582                            let prompt = this.on_prompt_for_open_path.take().unwrap();
1583                            let rx = prompt(this, lister, cx);
1584                            this.on_prompt_for_open_path = Some(prompt);
1585                            rx
1586                        })?;
1587                        if let Ok(path) = rx.await {
1588                            tx.send(path).log_err();
1589                        }
1590                    }
1591                };
1592                anyhow::Ok(())
1593            })
1594            .detach();
1595
1596            rx
1597        }
1598    }
1599
1600    pub fn prompt_for_new_path(
1601        &mut self,
1602        cx: &mut ViewContext<Self>,
1603    ) -> oneshot::Receiver<Option<ProjectPath>> {
1604        if (self.project.read(cx).is_via_collab() || self.project.read(cx).is_via_ssh())
1605            || !WorkspaceSettings::get_global(cx).use_system_path_prompts
1606        {
1607            let prompt = self.on_prompt_for_new_path.take().unwrap();
1608            let rx = prompt(self, cx);
1609            self.on_prompt_for_new_path = Some(prompt);
1610            rx
1611        } else {
1612            let start_abs_path = self
1613                .project
1614                .update(cx, |project, cx| {
1615                    let worktree = project.visible_worktrees(cx).next()?;
1616                    Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
1617                })
1618                .unwrap_or_else(|| Path::new("").into());
1619
1620            let (tx, rx) = oneshot::channel();
1621            let abs_path = cx.prompt_for_new_path(&start_abs_path);
1622            cx.spawn(|this, mut cx| async move {
1623                let abs_path = match abs_path.await? {
1624                    Ok(path) => path,
1625                    Err(err) => {
1626                        let rx = this.update(&mut cx, |this, cx| {
1627                            this.show_portal_error(err.to_string(), cx);
1628
1629                            let prompt = this.on_prompt_for_new_path.take().unwrap();
1630                            let rx = prompt(this, cx);
1631                            this.on_prompt_for_new_path = Some(prompt);
1632                            rx
1633                        })?;
1634                        if let Ok(path) = rx.await {
1635                            tx.send(path).log_err();
1636                        }
1637                        return anyhow::Ok(());
1638                    }
1639                };
1640
1641                let project_path = abs_path.and_then(|abs_path| {
1642                    this.update(&mut cx, |this, cx| {
1643                        this.project.update(cx, |project, cx| {
1644                            project.find_or_create_worktree(abs_path, true, cx)
1645                        })
1646                    })
1647                    .ok()
1648                });
1649
1650                if let Some(project_path) = project_path {
1651                    let (worktree, path) = project_path.await?;
1652                    let worktree_id = worktree.read_with(&cx, |worktree, _| worktree.id())?;
1653                    tx.send(Some(ProjectPath {
1654                        worktree_id,
1655                        path: path.into(),
1656                    }))
1657                    .ok();
1658                } else {
1659                    tx.send(None).ok();
1660                }
1661                anyhow::Ok(())
1662            })
1663            .detach_and_log_err(cx);
1664
1665            rx
1666        }
1667    }
1668
1669    pub fn titlebar_item(&self) -> Option<AnyView> {
1670        self.titlebar_item.clone()
1671    }
1672
1673    /// Call the given callback with a workspace whose project is local.
1674    ///
1675    /// If the given workspace has a local project, then it will be passed
1676    /// to the callback. Otherwise, a new empty window will be created.
1677    pub fn with_local_workspace<T, F>(
1678        &mut self,
1679        cx: &mut ViewContext<Self>,
1680        callback: F,
1681    ) -> Task<Result<T>>
1682    where
1683        T: 'static,
1684        F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1685    {
1686        if self.project.read(cx).is_local() {
1687            Task::ready(Ok(callback(self, cx)))
1688        } else {
1689            let env = self.project.read(cx).cli_environment(cx);
1690            let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx);
1691            cx.spawn(|_vh, mut cx| async move {
1692                let (workspace, _) = task.await?;
1693                workspace.update(&mut cx, callback)
1694            })
1695        }
1696    }
1697
1698    pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Model<Worktree>> {
1699        self.project.read(cx).worktrees(cx)
1700    }
1701
1702    pub fn visible_worktrees<'a>(
1703        &self,
1704        cx: &'a AppContext,
1705    ) -> impl 'a + Iterator<Item = Model<Worktree>> {
1706        self.project.read(cx).visible_worktrees(cx)
1707    }
1708
1709    pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1710        let futures = self
1711            .worktrees(cx)
1712            .filter_map(|worktree| worktree.read(cx).as_local())
1713            .map(|worktree| worktree.scan_complete())
1714            .collect::<Vec<_>>();
1715        async move {
1716            for future in futures {
1717                future.await;
1718            }
1719        }
1720    }
1721
1722    pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1723        cx.defer(|cx| {
1724            cx.windows().iter().find(|window| {
1725                window
1726                    .update(cx, |_, cx| {
1727                        if cx.is_window_active() {
1728                            //This can only get called when the window's project connection has been lost
1729                            //so we don't need to prompt the user for anything and instead just close the window
1730                            cx.remove_window();
1731                            true
1732                        } else {
1733                            false
1734                        }
1735                    })
1736                    .unwrap_or(false)
1737            });
1738        });
1739    }
1740
1741    pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext<Self>) {
1742        let prepare = self.prepare_to_close(CloseIntent::CloseWindow, cx);
1743        let window = cx.window_handle();
1744        cx.spawn(|_, mut cx| async move {
1745            if prepare.await? {
1746                window.update(&mut cx, |_, cx| {
1747                    cx.remove_window();
1748                })?;
1749            }
1750            anyhow::Ok(())
1751        })
1752        .detach_and_log_err(cx)
1753    }
1754
1755    pub fn move_focused_panel_to_next_position(
1756        &mut self,
1757        _: &MoveFocusedPanelToNextPosition,
1758        cx: &mut ViewContext<Self>,
1759    ) {
1760        let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1761        let active_dock = docks
1762            .into_iter()
1763            .find(|dock| dock.focus_handle(cx).contains_focused(cx));
1764
1765        if let Some(dock) = active_dock {
1766            dock.update(cx, |dock, cx| {
1767                let active_panel = dock
1768                    .active_panel()
1769                    .filter(|panel| panel.focus_handle(cx).contains_focused(cx));
1770
1771                if let Some(panel) = active_panel {
1772                    panel.move_to_next_position(cx);
1773                }
1774            })
1775        }
1776    }
1777
1778    pub fn prepare_to_close(
1779        &mut self,
1780        close_intent: CloseIntent,
1781        cx: &mut ViewContext<Self>,
1782    ) -> Task<Result<bool>> {
1783        let active_call = self.active_call().cloned();
1784        let window = cx.window_handle();
1785
1786        // On Linux and Windows, closing the last window should restore the last workspace.
1787        let save_last_workspace = cfg!(not(target_os = "macos"))
1788            && close_intent != CloseIntent::ReplaceWindow
1789            && cx.windows().len() == 1;
1790
1791        cx.spawn(|this, mut cx| async move {
1792            let workspace_count = (*cx).update(|cx| {
1793                cx.windows()
1794                    .iter()
1795                    .filter(|window| window.downcast::<Workspace>().is_some())
1796                    .count()
1797            })?;
1798
1799            if let Some(active_call) = active_call {
1800                if close_intent != CloseIntent::Quit
1801                    && workspace_count == 1
1802                    && active_call.read_with(&cx, |call, _| call.room().is_some())?
1803                {
1804                    let answer = window.update(&mut cx, |_, cx| {
1805                        cx.prompt(
1806                            PromptLevel::Warning,
1807                            "Do you want to leave the current call?",
1808                            None,
1809                            &["Close window and hang up", "Cancel"],
1810                        )
1811                    })?;
1812
1813                    if answer.await.log_err() == Some(1) {
1814                        return anyhow::Ok(false);
1815                    } else {
1816                        active_call
1817                            .update(&mut cx, |call, cx| call.hang_up(cx))?
1818                            .await
1819                            .log_err();
1820                    }
1821                }
1822            }
1823
1824            let save_result = this
1825                .update(&mut cx, |this, cx| {
1826                    this.save_all_internal(SaveIntent::Close, cx)
1827                })?
1828                .await;
1829
1830            // If we're not quitting, but closing, we remove the workspace from
1831            // the current session.
1832            if close_intent != CloseIntent::Quit
1833                && !save_last_workspace
1834                && save_result.as_ref().map_or(false, |&res| res)
1835            {
1836                this.update(&mut cx, |this, cx| this.remove_from_session(cx))?
1837                    .await;
1838            }
1839
1840            save_result
1841        })
1842    }
1843
1844    fn save_all(&mut self, action: &SaveAll, cx: &mut ViewContext<Self>) {
1845        self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx)
1846            .detach_and_log_err(cx);
1847    }
1848
1849    fn send_keystrokes(&mut self, action: &SendKeystrokes, cx: &mut ViewContext<Self>) {
1850        let mut state = self.dispatching_keystrokes.borrow_mut();
1851        if !state.0.insert(action.0.clone()) {
1852            cx.propagate();
1853            return;
1854        }
1855        let mut keystrokes: Vec<Keystroke> = action
1856            .0
1857            .split(' ')
1858            .flat_map(|k| Keystroke::parse(k).log_err())
1859            .collect();
1860        keystrokes.reverse();
1861
1862        state.1.append(&mut keystrokes);
1863        drop(state);
1864
1865        let keystrokes = self.dispatching_keystrokes.clone();
1866        cx.window_context()
1867            .spawn(|mut cx| async move {
1868                // limit to 100 keystrokes to avoid infinite recursion.
1869                for _ in 0..100 {
1870                    let Some(keystroke) = keystrokes.borrow_mut().1.pop() else {
1871                        keystrokes.borrow_mut().0.clear();
1872                        return Ok(());
1873                    };
1874                    cx.update(|cx| {
1875                        let focused = cx.focused();
1876                        cx.dispatch_keystroke(keystroke.clone());
1877                        if cx.focused() != focused {
1878                            // dispatch_keystroke may cause the focus to change.
1879                            // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
1880                            // And we need that to happen before the next keystroke to keep vim mode happy...
1881                            // (Note that the tests always do this implicitly, so you must manually test with something like:
1882                            //   "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
1883                            // )
1884                            cx.draw();
1885                        }
1886                    })?;
1887                }
1888
1889                *keystrokes.borrow_mut() = Default::default();
1890                Err(anyhow!("over 100 keystrokes passed to send_keystrokes"))
1891            })
1892            .detach_and_log_err(cx);
1893    }
1894
1895    fn save_all_internal(
1896        &mut self,
1897        mut save_intent: SaveIntent,
1898        cx: &mut ViewContext<Self>,
1899    ) -> Task<Result<bool>> {
1900        if self.project.read(cx).is_disconnected(cx) {
1901            return Task::ready(Ok(true));
1902        }
1903        let dirty_items = self
1904            .panes
1905            .iter()
1906            .flat_map(|pane| {
1907                pane.read(cx).items().filter_map(|item| {
1908                    if item.is_dirty(cx) {
1909                        item.tab_description(0, cx);
1910                        Some((pane.downgrade(), item.boxed_clone()))
1911                    } else {
1912                        None
1913                    }
1914                })
1915            })
1916            .collect::<Vec<_>>();
1917
1918        let project = self.project.clone();
1919        cx.spawn(|workspace, mut cx| async move {
1920            let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
1921                let (serialize_tasks, remaining_dirty_items) =
1922                    workspace.update(&mut cx, |workspace, cx| {
1923                        let mut remaining_dirty_items = Vec::new();
1924                        let mut serialize_tasks = Vec::new();
1925                        for (pane, item) in dirty_items {
1926                            if let Some(task) = item
1927                                .to_serializable_item_handle(cx)
1928                                .and_then(|handle| handle.serialize(workspace, true, cx))
1929                            {
1930                                serialize_tasks.push(task);
1931                            } else {
1932                                remaining_dirty_items.push((pane, item));
1933                            }
1934                        }
1935                        (serialize_tasks, remaining_dirty_items)
1936                    })?;
1937
1938                futures::future::try_join_all(serialize_tasks).await?;
1939
1940                if remaining_dirty_items.len() > 1 {
1941                    let answer = workspace.update(&mut cx, |_, cx| {
1942                        let (prompt, detail) = Pane::file_names_for_prompt(
1943                            &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
1944                            remaining_dirty_items.len(),
1945                            cx,
1946                        );
1947                        cx.prompt(
1948                            PromptLevel::Warning,
1949                            &prompt,
1950                            Some(&detail),
1951                            &["Save all", "Discard all", "Cancel"],
1952                        )
1953                    })?;
1954                    match answer.await.log_err() {
1955                        Some(0) => save_intent = SaveIntent::SaveAll,
1956                        Some(1) => save_intent = SaveIntent::Skip,
1957                        _ => {}
1958                    }
1959                }
1960
1961                remaining_dirty_items
1962            } else {
1963                dirty_items
1964            };
1965
1966            for (pane, item) in dirty_items {
1967                let (singleton, project_entry_ids) =
1968                    cx.update(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
1969                if singleton || !project_entry_ids.is_empty() {
1970                    if let Some(ix) =
1971                        pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))?
1972                    {
1973                        if !Pane::save_item(
1974                            project.clone(),
1975                            &pane,
1976                            ix,
1977                            &*item,
1978                            save_intent,
1979                            &mut cx,
1980                        )
1981                        .await?
1982                        {
1983                            return Ok(false);
1984                        }
1985                    }
1986                }
1987            }
1988            Ok(true)
1989        })
1990    }
1991
1992    pub fn open_workspace_for_paths(
1993        &mut self,
1994        replace_current_window: bool,
1995        paths: Vec<PathBuf>,
1996        cx: &mut ViewContext<Self>,
1997    ) -> Task<Result<()>> {
1998        let window = cx.window_handle().downcast::<Self>();
1999        let is_remote = self.project.read(cx).is_via_collab();
2000        let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
2001        let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
2002
2003        let window_to_replace = if replace_current_window {
2004            window
2005        } else if is_remote || has_worktree || has_dirty_items {
2006            None
2007        } else {
2008            window
2009        };
2010        let app_state = self.app_state.clone();
2011
2012        cx.spawn(|_, mut cx| async move {
2013            cx.update(|cx| {
2014                open_paths(
2015                    &paths,
2016                    app_state,
2017                    OpenOptions {
2018                        replace_window: window_to_replace,
2019                        ..Default::default()
2020                    },
2021                    cx,
2022                )
2023            })?
2024            .await?;
2025            Ok(())
2026        })
2027    }
2028
2029    #[allow(clippy::type_complexity)]
2030    pub fn open_paths(
2031        &mut self,
2032        mut abs_paths: Vec<PathBuf>,
2033        visible: OpenVisible,
2034        pane: Option<WeakView<Pane>>,
2035        cx: &mut ViewContext<Self>,
2036    ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
2037        log::info!("open paths {abs_paths:?}");
2038
2039        let fs = self.app_state.fs.clone();
2040
2041        // Sort the paths to ensure we add worktrees for parents before their children.
2042        abs_paths.sort_unstable();
2043        cx.spawn(move |this, mut cx| async move {
2044            let mut tasks = Vec::with_capacity(abs_paths.len());
2045
2046            for abs_path in &abs_paths {
2047                let visible = match visible {
2048                    OpenVisible::All => Some(true),
2049                    OpenVisible::None => Some(false),
2050                    OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
2051                        Some(Some(metadata)) => Some(!metadata.is_dir),
2052                        Some(None) => Some(true),
2053                        None => None,
2054                    },
2055                    OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
2056                        Some(Some(metadata)) => Some(metadata.is_dir),
2057                        Some(None) => Some(false),
2058                        None => None,
2059                    },
2060                };
2061                let project_path = match visible {
2062                    Some(visible) => match this
2063                        .update(&mut cx, |this, cx| {
2064                            Workspace::project_path_for_path(
2065                                this.project.clone(),
2066                                abs_path,
2067                                visible,
2068                                cx,
2069                            )
2070                        })
2071                        .log_err()
2072                    {
2073                        Some(project_path) => project_path.await.log_err(),
2074                        None => None,
2075                    },
2076                    None => None,
2077                };
2078
2079                let this = this.clone();
2080                let abs_path: Arc<Path> = SanitizedPath::from(abs_path.clone()).into();
2081                let fs = fs.clone();
2082                let pane = pane.clone();
2083                let task = cx.spawn(move |mut cx| async move {
2084                    let (worktree, project_path) = project_path?;
2085                    if fs.is_dir(&abs_path).await {
2086                        this.update(&mut cx, |workspace, cx| {
2087                            let worktree = worktree.read(cx);
2088                            let worktree_abs_path = worktree.abs_path();
2089                            let entry_id = if abs_path.as_ref() == worktree_abs_path.as_ref() {
2090                                worktree.root_entry()
2091                            } else {
2092                                abs_path
2093                                    .strip_prefix(worktree_abs_path.as_ref())
2094                                    .ok()
2095                                    .and_then(|relative_path| {
2096                                        worktree.entry_for_path(relative_path)
2097                                    })
2098                            }
2099                            .map(|entry| entry.id);
2100                            if let Some(entry_id) = entry_id {
2101                                workspace.project.update(cx, |_, cx| {
2102                                    cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
2103                                })
2104                            }
2105                        })
2106                        .log_err()?;
2107                        None
2108                    } else {
2109                        Some(
2110                            this.update(&mut cx, |this, cx| {
2111                                this.open_path(project_path, pane, true, cx)
2112                            })
2113                            .log_err()?
2114                            .await,
2115                        )
2116                    }
2117                });
2118                tasks.push(task);
2119            }
2120
2121            futures::future::join_all(tasks).await
2122        })
2123    }
2124
2125    pub fn open_resolved_path(
2126        &mut self,
2127        path: ResolvedPath,
2128        cx: &mut ViewContext<Self>,
2129    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2130        match path {
2131            ResolvedPath::ProjectPath { project_path, .. } => {
2132                self.open_path(project_path, None, true, cx)
2133            }
2134            ResolvedPath::AbsPath { path, .. } => self.open_abs_path(path, false, cx),
2135        }
2136    }
2137
2138    fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
2139        let project = self.project.read(cx);
2140        if project.is_via_collab() {
2141            self.show_error(
2142                &anyhow!("You cannot add folders to someone else's project"),
2143                cx,
2144            );
2145            return;
2146        }
2147        let paths = self.prompt_for_open_path(
2148            PathPromptOptions {
2149                files: false,
2150                directories: true,
2151                multiple: true,
2152            },
2153            DirectoryLister::Project(self.project.clone()),
2154            cx,
2155        );
2156        cx.spawn(|this, mut cx| async move {
2157            if let Some(paths) = paths.await.log_err().flatten() {
2158                let results = this
2159                    .update(&mut cx, |this, cx| {
2160                        this.open_paths(paths, OpenVisible::All, None, cx)
2161                    })?
2162                    .await;
2163                for result in results.into_iter().flatten() {
2164                    result.log_err();
2165                }
2166            }
2167            anyhow::Ok(())
2168        })
2169        .detach_and_log_err(cx);
2170    }
2171
2172    pub fn project_path_for_path(
2173        project: Model<Project>,
2174        abs_path: &Path,
2175        visible: bool,
2176        cx: &mut AppContext,
2177    ) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
2178        let entry = project.update(cx, |project, cx| {
2179            project.find_or_create_worktree(abs_path, visible, cx)
2180        });
2181        cx.spawn(|mut cx| async move {
2182            let (worktree, path) = entry.await?;
2183            let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
2184            Ok((
2185                worktree,
2186                ProjectPath {
2187                    worktree_id,
2188                    path: path.into(),
2189                },
2190            ))
2191        })
2192    }
2193
2194    pub fn items<'a>(
2195        &'a self,
2196        cx: &'a AppContext,
2197    ) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
2198        self.panes.iter().flat_map(|pane| pane.read(cx).items())
2199    }
2200
2201    pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
2202        self.items_of_type(cx).max_by_key(|item| item.item_id())
2203    }
2204
2205    pub fn items_of_type<'a, T: Item>(
2206        &'a self,
2207        cx: &'a AppContext,
2208    ) -> impl 'a + Iterator<Item = View<T>> {
2209        self.panes
2210            .iter()
2211            .flat_map(|pane| pane.read(cx).items_of_type())
2212    }
2213
2214    pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
2215        self.active_pane().read(cx).active_item()
2216    }
2217
2218    pub fn active_item_as<I: 'static>(&self, cx: &AppContext) -> Option<View<I>> {
2219        let item = self.active_item(cx)?;
2220        item.to_any().downcast::<I>().ok()
2221    }
2222
2223    fn active_project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
2224        self.active_item(cx).and_then(|item| item.project_path(cx))
2225    }
2226
2227    pub fn save_active_item(
2228        &mut self,
2229        save_intent: SaveIntent,
2230        cx: &mut WindowContext,
2231    ) -> Task<Result<()>> {
2232        let project = self.project.clone();
2233        let pane = self.active_pane();
2234        let item_ix = pane.read(cx).active_item_index();
2235        let item = pane.read(cx).active_item();
2236        let pane = pane.downgrade();
2237
2238        cx.spawn(|mut cx| async move {
2239            if let Some(item) = item {
2240                Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
2241                    .await
2242                    .map(|_| ())
2243            } else {
2244                Ok(())
2245            }
2246        })
2247    }
2248
2249    pub fn close_inactive_items_and_panes(
2250        &mut self,
2251        action: &CloseInactiveTabsAndPanes,
2252        cx: &mut ViewContext<Self>,
2253    ) {
2254        if let Some(task) =
2255            self.close_all_internal(true, action.save_intent.unwrap_or(SaveIntent::Close), cx)
2256        {
2257            task.detach_and_log_err(cx)
2258        }
2259    }
2260
2261    pub fn close_all_items_and_panes(
2262        &mut self,
2263        action: &CloseAllItemsAndPanes,
2264        cx: &mut ViewContext<Self>,
2265    ) {
2266        if let Some(task) =
2267            self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
2268        {
2269            task.detach_and_log_err(cx)
2270        }
2271    }
2272
2273    fn close_all_internal(
2274        &mut self,
2275        retain_active_pane: bool,
2276        save_intent: SaveIntent,
2277        cx: &mut ViewContext<Self>,
2278    ) -> Option<Task<Result<()>>> {
2279        let current_pane = self.active_pane();
2280
2281        let mut tasks = Vec::new();
2282
2283        if retain_active_pane {
2284            if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
2285                pane.close_inactive_items(
2286                    &CloseInactiveItems {
2287                        save_intent: None,
2288                        close_pinned: false,
2289                    },
2290                    cx,
2291                )
2292            }) {
2293                tasks.push(current_pane_close);
2294            };
2295        }
2296
2297        for pane in self.panes() {
2298            if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
2299                continue;
2300            }
2301
2302            if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
2303                pane.close_all_items(
2304                    &CloseAllItems {
2305                        save_intent: Some(save_intent),
2306                        close_pinned: false,
2307                    },
2308                    cx,
2309                )
2310            }) {
2311                tasks.push(close_pane_items)
2312            }
2313        }
2314
2315        if tasks.is_empty() {
2316            None
2317        } else {
2318            Some(cx.spawn(|_, _| async move {
2319                for task in tasks {
2320                    task.await?
2321                }
2322                Ok(())
2323            }))
2324        }
2325    }
2326
2327    pub fn is_dock_at_position_open(
2328        &self,
2329        position: DockPosition,
2330        cx: &mut ViewContext<Self>,
2331    ) -> bool {
2332        let dock = match position {
2333            DockPosition::Left => &self.left_dock,
2334            DockPosition::Bottom => &self.bottom_dock,
2335            DockPosition::Right => &self.right_dock,
2336        };
2337        dock.read(cx).is_open()
2338    }
2339
2340    pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
2341        let dock = match dock_side {
2342            DockPosition::Left => &self.left_dock,
2343            DockPosition::Bottom => &self.bottom_dock,
2344            DockPosition::Right => &self.right_dock,
2345        };
2346        let mut focus_center = false;
2347        let mut reveal_dock = false;
2348        dock.update(cx, |dock, cx| {
2349            let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
2350            let was_visible = dock.is_open() && !other_is_zoomed;
2351            dock.set_open(!was_visible, cx);
2352
2353            if dock.active_panel().is_none() && dock.panels_len() > 0 {
2354                dock.activate_panel(0, cx);
2355            }
2356
2357            if let Some(active_panel) = dock.active_panel() {
2358                if was_visible {
2359                    if active_panel.focus_handle(cx).contains_focused(cx) {
2360                        focus_center = true;
2361                    }
2362                } else {
2363                    let focus_handle = &active_panel.focus_handle(cx);
2364                    cx.focus(focus_handle);
2365                    reveal_dock = true;
2366                }
2367            }
2368        });
2369
2370        if reveal_dock {
2371            self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
2372        }
2373
2374        if focus_center {
2375            self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2376        }
2377
2378        cx.notify();
2379        self.serialize_workspace(cx);
2380    }
2381
2382    pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
2383        let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
2384
2385        for dock in docks {
2386            dock.update(cx, |dock, cx| {
2387                dock.set_open(false, cx);
2388            });
2389        }
2390
2391        cx.focus_self();
2392        cx.notify();
2393        self.serialize_workspace(cx);
2394    }
2395
2396    /// Transfer focus to the panel of the given type.
2397    pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
2398        let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
2399        panel.to_any().downcast().ok()
2400    }
2401
2402    /// Focus the panel of the given type if it isn't already focused. If it is
2403    /// already focused, then transfer focus back to the workspace center.
2404    pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2405        self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
2406            !panel.focus_handle(cx).contains_focused(cx)
2407        });
2408    }
2409
2410    pub fn activate_panel_for_proto_id(
2411        &mut self,
2412        panel_id: PanelId,
2413        cx: &mut ViewContext<Self>,
2414    ) -> Option<Arc<dyn PanelHandle>> {
2415        let mut panel = None;
2416        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2417            if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
2418                panel = dock.update(cx, |dock, cx| {
2419                    dock.activate_panel(panel_index, cx);
2420                    dock.set_open(true, cx);
2421                    dock.active_panel().cloned()
2422                });
2423                break;
2424            }
2425        }
2426
2427        if panel.is_some() {
2428            cx.notify();
2429            self.serialize_workspace(cx);
2430        }
2431
2432        panel
2433    }
2434
2435    /// Focus or unfocus the given panel type, depending on the given callback.
2436    fn focus_or_unfocus_panel<T: Panel>(
2437        &mut self,
2438        cx: &mut ViewContext<Self>,
2439        should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
2440    ) -> Option<Arc<dyn PanelHandle>> {
2441        let mut result_panel = None;
2442        let mut serialize = false;
2443        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2444            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2445                let mut focus_center = false;
2446                let panel = dock.update(cx, |dock, cx| {
2447                    dock.activate_panel(panel_index, cx);
2448
2449                    let panel = dock.active_panel().cloned();
2450                    if let Some(panel) = panel.as_ref() {
2451                        if should_focus(&**panel, cx) {
2452                            dock.set_open(true, cx);
2453                            panel.focus_handle(cx).focus(cx);
2454                        } else {
2455                            focus_center = true;
2456                        }
2457                    }
2458                    panel
2459                });
2460
2461                if focus_center {
2462                    self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2463                }
2464
2465                result_panel = panel;
2466                serialize = true;
2467                break;
2468            }
2469        }
2470
2471        if serialize {
2472            self.serialize_workspace(cx);
2473        }
2474
2475        cx.notify();
2476        result_panel
2477    }
2478
2479    /// Open the panel of the given type
2480    pub fn open_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2481        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2482            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2483                dock.update(cx, |dock, cx| {
2484                    dock.activate_panel(panel_index, cx);
2485                    dock.set_open(true, cx);
2486                });
2487            }
2488        }
2489    }
2490
2491    pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
2492        [&self.left_dock, &self.bottom_dock, &self.right_dock]
2493            .iter()
2494            .find_map(|dock| dock.read(cx).panel::<T>())
2495    }
2496
2497    fn dismiss_zoomed_items_to_reveal(
2498        &mut self,
2499        dock_to_reveal: Option<DockPosition>,
2500        cx: &mut ViewContext<Self>,
2501    ) {
2502        // If a center pane is zoomed, unzoom it.
2503        for pane in &self.panes {
2504            if pane != &self.active_pane || dock_to_reveal.is_some() {
2505                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2506            }
2507        }
2508
2509        // If another dock is zoomed, hide it.
2510        let mut focus_center = false;
2511        for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
2512            dock.update(cx, |dock, cx| {
2513                if Some(dock.position()) != dock_to_reveal {
2514                    if let Some(panel) = dock.active_panel() {
2515                        if panel.is_zoomed(cx) {
2516                            focus_center |= panel.focus_handle(cx).contains_focused(cx);
2517                            dock.set_open(false, cx);
2518                        }
2519                    }
2520                }
2521            });
2522        }
2523
2524        if focus_center {
2525            self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2526        }
2527
2528        if self.zoomed_position != dock_to_reveal {
2529            self.zoomed = None;
2530            self.zoomed_position = None;
2531            cx.emit(Event::ZoomChanged);
2532        }
2533
2534        cx.notify();
2535    }
2536
2537    fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2538        let pane = cx.new_view(|cx| {
2539            let mut pane = Pane::new(
2540                self.weak_handle(),
2541                self.project.clone(),
2542                self.pane_history_timestamp.clone(),
2543                None,
2544                NewFile.boxed_clone(),
2545                cx,
2546            );
2547            pane.set_can_split(Some(Arc::new(|_, _, _| true)));
2548            pane
2549        });
2550        cx.subscribe(&pane, Self::handle_pane_event).detach();
2551        self.panes.push(pane.clone());
2552        cx.focus_view(&pane);
2553        cx.emit(Event::PaneAdded(pane.clone()));
2554        pane
2555    }
2556
2557    pub fn add_item_to_center(
2558        &mut self,
2559        item: Box<dyn ItemHandle>,
2560        cx: &mut ViewContext<Self>,
2561    ) -> bool {
2562        if let Some(center_pane) = self.last_active_center_pane.clone() {
2563            if let Some(center_pane) = center_pane.upgrade() {
2564                center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
2565                true
2566            } else {
2567                false
2568            }
2569        } else {
2570            false
2571        }
2572    }
2573
2574    pub fn add_item_to_active_pane(
2575        &mut self,
2576        item: Box<dyn ItemHandle>,
2577        destination_index: Option<usize>,
2578        focus_item: bool,
2579        cx: &mut WindowContext,
2580    ) {
2581        self.add_item(
2582            self.active_pane.clone(),
2583            item,
2584            destination_index,
2585            false,
2586            focus_item,
2587            cx,
2588        )
2589    }
2590
2591    pub fn add_item(
2592        &mut self,
2593        pane: View<Pane>,
2594        item: Box<dyn ItemHandle>,
2595        destination_index: Option<usize>,
2596        activate_pane: bool,
2597        focus_item: bool,
2598        cx: &mut WindowContext,
2599    ) {
2600        if let Some(text) = item.telemetry_event_text(cx) {
2601            self.client()
2602                .telemetry()
2603                .report_app_event(format!("{}: open", text));
2604        }
2605
2606        pane.update(cx, |pane, cx| {
2607            pane.add_item(item, activate_pane, focus_item, destination_index, cx)
2608        });
2609    }
2610
2611    pub fn split_item(
2612        &mut self,
2613        split_direction: SplitDirection,
2614        item: Box<dyn ItemHandle>,
2615        cx: &mut ViewContext<Self>,
2616    ) {
2617        let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
2618        self.add_item(new_pane, item, None, true, true, cx);
2619    }
2620
2621    pub fn open_abs_path(
2622        &mut self,
2623        abs_path: PathBuf,
2624        visible: bool,
2625        cx: &mut ViewContext<Self>,
2626    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2627        cx.spawn(|workspace, mut cx| async move {
2628            let open_paths_task_result = workspace
2629                .update(&mut cx, |workspace, cx| {
2630                    workspace.open_paths(
2631                        vec![abs_path.clone()],
2632                        if visible {
2633                            OpenVisible::All
2634                        } else {
2635                            OpenVisible::None
2636                        },
2637                        None,
2638                        cx,
2639                    )
2640                })
2641                .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2642                .await;
2643            anyhow::ensure!(
2644                open_paths_task_result.len() == 1,
2645                "open abs path {abs_path:?} task returned incorrect number of results"
2646            );
2647            match open_paths_task_result
2648                .into_iter()
2649                .next()
2650                .expect("ensured single task result")
2651            {
2652                Some(open_result) => {
2653                    open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2654                }
2655                None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2656            }
2657        })
2658    }
2659
2660    pub fn split_abs_path(
2661        &mut self,
2662        abs_path: PathBuf,
2663        visible: bool,
2664        cx: &mut ViewContext<Self>,
2665    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2666        let project_path_task =
2667            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2668        cx.spawn(|this, mut cx| async move {
2669            let (_, path) = project_path_task.await?;
2670            this.update(&mut cx, |this, cx| this.split_path(path, cx))?
2671                .await
2672        })
2673    }
2674
2675    pub fn open_path(
2676        &mut self,
2677        path: impl Into<ProjectPath>,
2678        pane: Option<WeakView<Pane>>,
2679        focus_item: bool,
2680        cx: &mut WindowContext,
2681    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2682        self.open_path_preview(path, pane, focus_item, false, cx)
2683    }
2684
2685    pub fn open_path_preview(
2686        &mut self,
2687        path: impl Into<ProjectPath>,
2688        pane: Option<WeakView<Pane>>,
2689        focus_item: bool,
2690        allow_preview: bool,
2691        cx: &mut WindowContext,
2692    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2693        let pane = pane.unwrap_or_else(|| {
2694            self.last_active_center_pane.clone().unwrap_or_else(|| {
2695                self.panes
2696                    .first()
2697                    .expect("There must be an active pane")
2698                    .downgrade()
2699            })
2700        });
2701
2702        let task = self.load_path(path.into(), cx);
2703        cx.spawn(move |mut cx| async move {
2704            let (project_entry_id, build_item) = task.await?;
2705            pane.update(&mut cx, |pane, cx| {
2706                pane.open_item(
2707                    project_entry_id,
2708                    focus_item,
2709                    allow_preview,
2710                    None,
2711                    cx,
2712                    build_item,
2713                )
2714            })
2715        })
2716    }
2717
2718    pub fn split_path(
2719        &mut self,
2720        path: impl Into<ProjectPath>,
2721        cx: &mut ViewContext<Self>,
2722    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2723        self.split_path_preview(path, false, None, cx)
2724    }
2725
2726    pub fn split_path_preview(
2727        &mut self,
2728        path: impl Into<ProjectPath>,
2729        allow_preview: bool,
2730        split_direction: Option<SplitDirection>,
2731        cx: &mut ViewContext<Self>,
2732    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2733        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2734            self.panes
2735                .first()
2736                .expect("There must be an active pane")
2737                .downgrade()
2738        });
2739
2740        if let Member::Pane(center_pane) = &self.center.root {
2741            if center_pane.read(cx).items_len() == 0 {
2742                return self.open_path(path, Some(pane), true, cx);
2743            }
2744        }
2745
2746        let task = self.load_path(path.into(), cx);
2747        cx.spawn(|this, mut cx| async move {
2748            let (project_entry_id, build_item) = task.await?;
2749            this.update(&mut cx, move |this, cx| -> Option<_> {
2750                let pane = pane.upgrade()?;
2751                let new_pane =
2752                    this.split_pane(pane, split_direction.unwrap_or(SplitDirection::Right), cx);
2753                new_pane.update(cx, |new_pane, cx| {
2754                    Some(new_pane.open_item(
2755                        project_entry_id,
2756                        true,
2757                        allow_preview,
2758                        None,
2759                        cx,
2760                        build_item,
2761                    ))
2762                })
2763            })
2764            .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2765        })
2766    }
2767
2768    fn load_path(
2769        &mut self,
2770        path: ProjectPath,
2771        cx: &mut WindowContext,
2772    ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
2773        let project = self.project().clone();
2774        let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
2775        let Some(open_project_item) = project_item_builders
2776            .iter()
2777            .rev()
2778            .find_map(|open_project_item| open_project_item(&project, &path, cx))
2779        else {
2780            return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
2781        };
2782        open_project_item
2783    }
2784
2785    pub fn find_project_item<T>(
2786        &self,
2787        pane: &View<Pane>,
2788        project_item: &Model<T::Item>,
2789        cx: &AppContext,
2790    ) -> Option<View<T>>
2791    where
2792        T: ProjectItem,
2793    {
2794        use project::ProjectItem as _;
2795        let project_item = project_item.read(cx);
2796        let entry_id = project_item.entry_id(cx);
2797        let project_path = project_item.project_path(cx);
2798
2799        let mut item = None;
2800        if let Some(entry_id) = entry_id {
2801            item = pane.read(cx).item_for_entry(entry_id, cx);
2802        }
2803        if item.is_none() {
2804            if let Some(project_path) = project_path {
2805                item = pane.read(cx).item_for_path(project_path, cx);
2806            }
2807        }
2808
2809        item.and_then(|item| item.downcast::<T>())
2810    }
2811
2812    pub fn is_project_item_open<T>(
2813        &self,
2814        pane: &View<Pane>,
2815        project_item: &Model<T::Item>,
2816        cx: &AppContext,
2817    ) -> bool
2818    where
2819        T: ProjectItem,
2820    {
2821        self.find_project_item::<T>(pane, project_item, cx)
2822            .is_some()
2823    }
2824
2825    pub fn open_project_item<T>(
2826        &mut self,
2827        pane: View<Pane>,
2828        project_item: Model<T::Item>,
2829        activate_pane: bool,
2830        focus_item: bool,
2831        cx: &mut ViewContext<Self>,
2832    ) -> View<T>
2833    where
2834        T: ProjectItem,
2835    {
2836        if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
2837            self.activate_item(&item, activate_pane, focus_item, cx);
2838            return item;
2839        }
2840
2841        let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2842        let item_id = item.item_id();
2843        let mut destination_index = None;
2844        pane.update(cx, |pane, cx| {
2845            if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
2846                if let Some(preview_item_id) = pane.preview_item_id() {
2847                    if preview_item_id != item_id {
2848                        destination_index = pane.close_current_preview_item(cx);
2849                    }
2850                }
2851            }
2852            pane.set_preview_item_id(Some(item.item_id()), cx)
2853        });
2854
2855        self.add_item(
2856            pane,
2857            Box::new(item.clone()),
2858            destination_index,
2859            activate_pane,
2860            focus_item,
2861            cx,
2862        );
2863        item
2864    }
2865
2866    pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2867        if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2868            self.active_pane.update(cx, |pane, cx| {
2869                pane.add_item(Box::new(shared_screen), false, true, None, cx)
2870            });
2871        }
2872    }
2873
2874    pub fn activate_item(
2875        &mut self,
2876        item: &dyn ItemHandle,
2877        activate_pane: bool,
2878        focus_item: bool,
2879        cx: &mut WindowContext,
2880    ) -> bool {
2881        let result = self.panes.iter().find_map(|pane| {
2882            pane.read(cx)
2883                .index_for_item(item)
2884                .map(|ix| (pane.clone(), ix))
2885        });
2886        if let Some((pane, ix)) = result {
2887            pane.update(cx, |pane, cx| {
2888                pane.activate_item(ix, activate_pane, focus_item, cx)
2889            });
2890            true
2891        } else {
2892            false
2893        }
2894    }
2895
2896    fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2897        let panes = self.center.panes();
2898        if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2899            cx.focus_view(&pane);
2900        } else {
2901            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2902        }
2903    }
2904
2905    fn move_item_to_pane_at_index(&mut self, action: &MoveItemToPane, cx: &mut ViewContext<Self>) {
2906        let Some(&target_pane) = self.center.panes().get(action.destination) else {
2907            return;
2908        };
2909        move_active_item(&self.active_pane, target_pane, action.focus, true, cx);
2910    }
2911
2912    pub fn activate_next_pane(&mut self, cx: &mut WindowContext) {
2913        let panes = self.center.panes();
2914        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2915            let next_ix = (ix + 1) % panes.len();
2916            let next_pane = panes[next_ix].clone();
2917            cx.focus_view(&next_pane);
2918        }
2919    }
2920
2921    pub fn activate_previous_pane(&mut self, cx: &mut WindowContext) {
2922        let panes = self.center.panes();
2923        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2924            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2925            let prev_pane = panes[prev_ix].clone();
2926            cx.focus_view(&prev_pane);
2927        }
2928    }
2929
2930    pub fn activate_pane_in_direction(
2931        &mut self,
2932        direction: SplitDirection,
2933        cx: &mut WindowContext,
2934    ) {
2935        use ActivateInDirectionTarget as Target;
2936        enum Origin {
2937            LeftDock,
2938            RightDock,
2939            BottomDock,
2940            Center,
2941        }
2942
2943        let origin: Origin = [
2944            (&self.left_dock, Origin::LeftDock),
2945            (&self.right_dock, Origin::RightDock),
2946            (&self.bottom_dock, Origin::BottomDock),
2947        ]
2948        .into_iter()
2949        .find_map(|(dock, origin)| {
2950            if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
2951                Some(origin)
2952            } else {
2953                None
2954            }
2955        })
2956        .unwrap_or(Origin::Center);
2957
2958        let get_last_active_pane = || {
2959            let pane = self
2960                .last_active_center_pane
2961                .clone()
2962                .unwrap_or_else(|| {
2963                    self.panes
2964                        .first()
2965                        .expect("There must be an active pane")
2966                        .downgrade()
2967                })
2968                .upgrade()?;
2969            (pane.read(cx).items_len() != 0).then_some(pane)
2970        };
2971
2972        let try_dock =
2973            |dock: &View<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
2974
2975        let target = match (origin, direction) {
2976            // We're in the center, so we first try to go to a different pane,
2977            // otherwise try to go to a dock.
2978            (Origin::Center, direction) => {
2979                if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2980                    Some(Target::Pane(pane))
2981                } else {
2982                    match direction {
2983                        SplitDirection::Up => None,
2984                        SplitDirection::Down => try_dock(&self.bottom_dock),
2985                        SplitDirection::Left => try_dock(&self.left_dock),
2986                        SplitDirection::Right => try_dock(&self.right_dock),
2987                    }
2988                }
2989            }
2990
2991            (Origin::LeftDock, SplitDirection::Right) => {
2992                if let Some(last_active_pane) = get_last_active_pane() {
2993                    Some(Target::Pane(last_active_pane))
2994                } else {
2995                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
2996                }
2997            }
2998
2999            (Origin::LeftDock, SplitDirection::Down)
3000            | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
3001
3002            (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
3003            (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
3004            (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
3005
3006            (Origin::RightDock, SplitDirection::Left) => {
3007                if let Some(last_active_pane) = get_last_active_pane() {
3008                    Some(Target::Pane(last_active_pane))
3009                } else {
3010                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
3011                }
3012            }
3013
3014            _ => None,
3015        };
3016
3017        match target {
3018            Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
3019            Some(ActivateInDirectionTarget::Dock(dock)) => {
3020                // Defer this to avoid a panic when the dock's active panel is already on the stack.
3021                cx.defer(move |cx| {
3022                    let dock = dock.read(cx);
3023                    if let Some(panel) = dock.active_panel() {
3024                        panel.focus_handle(cx).focus(cx);
3025                    } else {
3026                        log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
3027                    }
3028                })
3029            }
3030            None => {}
3031        }
3032    }
3033
3034    pub fn move_item_to_pane_in_direction(
3035        &mut self,
3036        action: &MoveItemToPaneInDirection,
3037        cx: &mut WindowContext,
3038    ) {
3039        if let Some(destination) = self.find_pane_in_direction(action.direction, cx) {
3040            move_active_item(&self.active_pane, &destination, action.focus, true, cx);
3041        }
3042    }
3043
3044    pub fn bounding_box_for_pane(&self, pane: &View<Pane>) -> Option<Bounds<Pixels>> {
3045        self.center.bounding_box_for_pane(pane)
3046    }
3047
3048    pub fn find_pane_in_direction(
3049        &mut self,
3050        direction: SplitDirection,
3051        cx: &WindowContext,
3052    ) -> Option<View<Pane>> {
3053        self.center
3054            .find_pane_in_direction(&self.active_pane, direction, cx)
3055            .cloned()
3056    }
3057
3058    pub fn swap_pane_in_direction(
3059        &mut self,
3060        direction: SplitDirection,
3061        cx: &mut ViewContext<Self>,
3062    ) {
3063        if let Some(to) = self.find_pane_in_direction(direction, cx) {
3064            self.center.swap(&self.active_pane, &to);
3065            cx.notify();
3066        }
3067    }
3068
3069    pub fn resize_pane(&mut self, axis: gpui::Axis, amount: Pixels, cx: &mut ViewContext<Self>) {
3070        self.center
3071            .resize(&self.active_pane, axis, amount, &self.bounds);
3072        cx.notify();
3073    }
3074
3075    pub fn reset_pane_sizes(&mut self, cx: &mut ViewContext<Self>) {
3076        self.center.reset_pane_sizes();
3077        cx.notify();
3078    }
3079
3080    fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3081        // This is explicitly hoisted out of the following check for pane identity as
3082        // terminal panel panes are not registered as a center panes.
3083        self.status_bar.update(cx, |status_bar, cx| {
3084            status_bar.set_active_pane(&pane, cx);
3085        });
3086        if self.active_pane != pane {
3087            self.set_active_pane(&pane, cx);
3088        }
3089
3090        if self.last_active_center_pane.is_none() {
3091            self.last_active_center_pane = Some(pane.downgrade());
3092        }
3093
3094        self.dismiss_zoomed_items_to_reveal(None, cx);
3095        if pane.read(cx).is_zoomed() {
3096            self.zoomed = Some(pane.downgrade().into());
3097        } else {
3098            self.zoomed = None;
3099        }
3100        self.zoomed_position = None;
3101        cx.emit(Event::ZoomChanged);
3102        self.update_active_view_for_followers(cx);
3103        pane.model.update(cx, |pane, _| {
3104            pane.track_alternate_file_items();
3105        });
3106
3107        cx.notify();
3108    }
3109
3110    fn set_active_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) {
3111        self.active_pane = pane.clone();
3112        self.active_item_path_changed(cx);
3113        self.last_active_center_pane = Some(pane.downgrade());
3114    }
3115
3116    fn handle_panel_focused(&mut self, cx: &mut ViewContext<Self>) {
3117        self.update_active_view_for_followers(cx);
3118    }
3119
3120    fn handle_pane_event(
3121        &mut self,
3122        pane: View<Pane>,
3123        event: &pane::Event,
3124        cx: &mut ViewContext<Self>,
3125    ) {
3126        let mut serialize_workspace = true;
3127        match event {
3128            pane::Event::AddItem { item } => {
3129                item.added_to_pane(self, pane, cx);
3130                cx.emit(Event::ItemAdded {
3131                    item: item.boxed_clone(),
3132                });
3133            }
3134            pane::Event::Split(direction) => {
3135                self.split_and_clone(pane, *direction, cx);
3136            }
3137            pane::Event::JoinIntoNext => {
3138                self.join_pane_into_next(pane, cx);
3139            }
3140            pane::Event::JoinAll => {
3141                self.join_all_panes(cx);
3142            }
3143            pane::Event::Remove { focus_on_pane } => {
3144                self.remove_pane(pane, focus_on_pane.clone(), cx);
3145            }
3146            pane::Event::ActivateItem {
3147                local,
3148                focus_changed,
3149            } => {
3150                cx.on_next_frame(|_, cx| {
3151                    cx.invalidate_character_coordinates();
3152                });
3153
3154                pane.model.update(cx, |pane, _| {
3155                    pane.track_alternate_file_items();
3156                });
3157                if *local {
3158                    self.unfollow_in_pane(&pane, cx);
3159                }
3160                if &pane == self.active_pane() {
3161                    self.active_item_path_changed(cx);
3162                    self.update_active_view_for_followers(cx);
3163                }
3164                serialize_workspace = *focus_changed || &pane != self.active_pane();
3165            }
3166            pane::Event::UserSavedItem { item, save_intent } => {
3167                cx.emit(Event::UserSavedItem {
3168                    pane: pane.downgrade(),
3169                    item: item.boxed_clone(),
3170                    save_intent: *save_intent,
3171                });
3172                serialize_workspace = false;
3173            }
3174            pane::Event::ChangeItemTitle => {
3175                if pane == self.active_pane {
3176                    self.active_item_path_changed(cx);
3177                }
3178                self.update_window_edited(cx);
3179                serialize_workspace = false;
3180            }
3181            pane::Event::RemoveItem { .. } => {}
3182            pane::Event::RemovedItem { item_id } => {
3183                cx.emit(Event::ActiveItemChanged);
3184                self.update_window_edited(cx);
3185                if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
3186                    if entry.get().entity_id() == pane.entity_id() {
3187                        entry.remove();
3188                    }
3189                }
3190            }
3191            pane::Event::Focus => {
3192                cx.on_next_frame(|_, cx| {
3193                    cx.invalidate_character_coordinates();
3194                });
3195                self.handle_pane_focused(pane.clone(), cx);
3196            }
3197            pane::Event::ZoomIn => {
3198                if pane == self.active_pane {
3199                    pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3200                    if pane.read(cx).has_focus(cx) {
3201                        self.zoomed = Some(pane.downgrade().into());
3202                        self.zoomed_position = None;
3203                        cx.emit(Event::ZoomChanged);
3204                    }
3205                    cx.notify();
3206                }
3207            }
3208            pane::Event::ZoomOut => {
3209                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3210                if self.zoomed_position.is_none() {
3211                    self.zoomed = None;
3212                    cx.emit(Event::ZoomChanged);
3213                }
3214                cx.notify();
3215            }
3216        }
3217
3218        if serialize_workspace {
3219            self.serialize_workspace(cx);
3220        }
3221    }
3222
3223    pub fn unfollow_in_pane(
3224        &mut self,
3225        pane: &View<Pane>,
3226        cx: &mut ViewContext<Workspace>,
3227    ) -> Option<PeerId> {
3228        let leader_id = self.leader_for_pane(pane)?;
3229        self.unfollow(leader_id, cx);
3230        Some(leader_id)
3231    }
3232
3233    pub fn split_pane(
3234        &mut self,
3235        pane_to_split: View<Pane>,
3236        split_direction: SplitDirection,
3237        cx: &mut ViewContext<Self>,
3238    ) -> View<Pane> {
3239        let new_pane = self.add_pane(cx);
3240        self.center
3241            .split(&pane_to_split, &new_pane, split_direction)
3242            .unwrap();
3243        cx.notify();
3244        new_pane
3245    }
3246
3247    pub fn split_and_clone(
3248        &mut self,
3249        pane: View<Pane>,
3250        direction: SplitDirection,
3251        cx: &mut ViewContext<Self>,
3252    ) -> Option<View<Pane>> {
3253        let item = pane.read(cx).active_item()?;
3254        let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
3255            let new_pane = self.add_pane(cx);
3256            new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
3257            self.center.split(&pane, &new_pane, direction).unwrap();
3258            Some(new_pane)
3259        } else {
3260            None
3261        };
3262        cx.notify();
3263        maybe_pane_handle
3264    }
3265
3266    pub fn split_pane_with_item(
3267        &mut self,
3268        pane_to_split: WeakView<Pane>,
3269        split_direction: SplitDirection,
3270        from: WeakView<Pane>,
3271        item_id_to_move: EntityId,
3272        cx: &mut ViewContext<Self>,
3273    ) {
3274        let Some(pane_to_split) = pane_to_split.upgrade() else {
3275            return;
3276        };
3277        let Some(from) = from.upgrade() else {
3278            return;
3279        };
3280
3281        let new_pane = self.add_pane(cx);
3282        move_item(&from, &new_pane, item_id_to_move, 0, cx);
3283        self.center
3284            .split(&pane_to_split, &new_pane, split_direction)
3285            .unwrap();
3286        cx.notify();
3287    }
3288
3289    pub fn split_pane_with_project_entry(
3290        &mut self,
3291        pane_to_split: WeakView<Pane>,
3292        split_direction: SplitDirection,
3293        project_entry: ProjectEntryId,
3294        cx: &mut ViewContext<Self>,
3295    ) -> Option<Task<Result<()>>> {
3296        let pane_to_split = pane_to_split.upgrade()?;
3297        let new_pane = self.add_pane(cx);
3298        self.center
3299            .split(&pane_to_split, &new_pane, split_direction)
3300            .unwrap();
3301
3302        let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3303        let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
3304        Some(cx.foreground_executor().spawn(async move {
3305            task.await?;
3306            Ok(())
3307        }))
3308    }
3309
3310    pub fn join_all_panes(&mut self, cx: &mut ViewContext<Self>) {
3311        let active_item = self.active_pane.read(cx).active_item();
3312        for pane in &self.panes {
3313            join_pane_into_active(&self.active_pane, pane, cx);
3314        }
3315        if let Some(active_item) = active_item {
3316            self.activate_item(active_item.as_ref(), true, true, cx);
3317        }
3318        cx.notify();
3319    }
3320
3321    pub fn join_pane_into_next(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3322        let next_pane = self
3323            .find_pane_in_direction(SplitDirection::Right, cx)
3324            .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3325            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3326            .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3327        let Some(next_pane) = next_pane else {
3328            return;
3329        };
3330        move_all_items(&pane, &next_pane, cx);
3331        cx.notify();
3332    }
3333
3334    fn remove_pane(
3335        &mut self,
3336        pane: View<Pane>,
3337        focus_on: Option<View<Pane>>,
3338        cx: &mut ViewContext<Self>,
3339    ) {
3340        if self.center.remove(&pane).unwrap() {
3341            self.force_remove_pane(&pane, &focus_on, cx);
3342            self.unfollow_in_pane(&pane, cx);
3343            self.last_leaders_by_pane.remove(&pane.downgrade());
3344            for removed_item in pane.read(cx).items() {
3345                self.panes_by_item.remove(&removed_item.item_id());
3346            }
3347
3348            cx.notify();
3349        } else {
3350            self.active_item_path_changed(cx);
3351        }
3352        cx.emit(Event::PaneRemoved);
3353    }
3354
3355    pub fn panes(&self) -> &[View<Pane>] {
3356        &self.panes
3357    }
3358
3359    pub fn active_pane(&self) -> &View<Pane> {
3360        &self.active_pane
3361    }
3362
3363    pub fn focused_pane(&self, cx: &WindowContext) -> View<Pane> {
3364        for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3365            if dock.focus_handle(cx).contains_focused(cx) {
3366                if let Some(pane) = dock
3367                    .read(cx)
3368                    .active_panel()
3369                    .and_then(|panel| panel.pane(cx))
3370                {
3371                    return pane;
3372                }
3373            }
3374        }
3375        self.active_pane().clone()
3376    }
3377
3378    pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
3379        self.find_pane_in_direction(SplitDirection::Right, cx)
3380            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3381            .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
3382            .clone()
3383    }
3384
3385    pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
3386        let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3387        weak_pane.upgrade()
3388    }
3389
3390    fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
3391        self.follower_states.retain(|leader_id, state| {
3392            if *leader_id == peer_id {
3393                for item in state.items_by_leader_view_id.values() {
3394                    item.view.set_leader_peer_id(None, cx);
3395                }
3396                false
3397            } else {
3398                true
3399            }
3400        });
3401        cx.notify();
3402    }
3403
3404    pub fn start_following(
3405        &mut self,
3406        leader_id: PeerId,
3407        cx: &mut ViewContext<Self>,
3408    ) -> Option<Task<Result<()>>> {
3409        let pane = self.active_pane().clone();
3410
3411        self.last_leaders_by_pane
3412            .insert(pane.downgrade(), leader_id);
3413        self.unfollow(leader_id, cx);
3414        self.unfollow_in_pane(&pane, cx);
3415        self.follower_states.insert(
3416            leader_id,
3417            FollowerState {
3418                center_pane: pane.clone(),
3419                dock_pane: None,
3420                active_view_id: None,
3421                items_by_leader_view_id: Default::default(),
3422            },
3423        );
3424        cx.notify();
3425
3426        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3427        let project_id = self.project.read(cx).remote_id();
3428        let request = self.app_state.client.request(proto::Follow {
3429            room_id,
3430            project_id,
3431            leader_id: Some(leader_id),
3432        });
3433
3434        Some(cx.spawn(|this, mut cx| async move {
3435            let response = request.await?;
3436            this.update(&mut cx, |this, _| {
3437                let state = this
3438                    .follower_states
3439                    .get_mut(&leader_id)
3440                    .ok_or_else(|| anyhow!("following interrupted"))?;
3441                state.active_view_id = response
3442                    .active_view
3443                    .as_ref()
3444                    .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3445                Ok::<_, anyhow::Error>(())
3446            })??;
3447            if let Some(view) = response.active_view {
3448                Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
3449            }
3450            this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
3451            Ok(())
3452        }))
3453    }
3454
3455    pub fn follow_next_collaborator(
3456        &mut self,
3457        _: &FollowNextCollaborator,
3458        cx: &mut ViewContext<Self>,
3459    ) {
3460        let collaborators = self.project.read(cx).collaborators();
3461        let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3462            let mut collaborators = collaborators.keys().copied();
3463            for peer_id in collaborators.by_ref() {
3464                if peer_id == leader_id {
3465                    break;
3466                }
3467            }
3468            collaborators.next()
3469        } else if let Some(last_leader_id) =
3470            self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3471        {
3472            if collaborators.contains_key(last_leader_id) {
3473                Some(*last_leader_id)
3474            } else {
3475                None
3476            }
3477        } else {
3478            None
3479        };
3480
3481        let pane = self.active_pane.clone();
3482        let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3483        else {
3484            return;
3485        };
3486        if self.unfollow_in_pane(&pane, cx) == Some(leader_id) {
3487            return;
3488        }
3489        if let Some(task) = self.start_following(leader_id, cx) {
3490            task.detach_and_log_err(cx)
3491        }
3492    }
3493
3494    pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
3495        let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3496            return;
3497        };
3498        let room = room.read(cx);
3499        let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3500            return;
3501        };
3502
3503        let project = self.project.read(cx);
3504
3505        let other_project_id = match remote_participant.location {
3506            call::ParticipantLocation::External => None,
3507            call::ParticipantLocation::UnsharedProject => None,
3508            call::ParticipantLocation::SharedProject { project_id } => {
3509                if Some(project_id) == project.remote_id() {
3510                    None
3511                } else {
3512                    Some(project_id)
3513                }
3514            }
3515        };
3516
3517        // if they are active in another project, follow there.
3518        if let Some(project_id) = other_project_id {
3519            let app_state = self.app_state.clone();
3520            crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3521                .detach_and_log_err(cx);
3522        }
3523
3524        // if you're already following, find the right pane and focus it.
3525        if let Some(follower_state) = self.follower_states.get(&leader_id) {
3526            cx.focus_view(follower_state.pane());
3527            return;
3528        }
3529
3530        // Otherwise, follow.
3531        if let Some(task) = self.start_following(leader_id, cx) {
3532            task.detach_and_log_err(cx)
3533        }
3534    }
3535
3536    pub fn unfollow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3537        cx.notify();
3538        let state = self.follower_states.remove(&leader_id)?;
3539        for (_, item) in state.items_by_leader_view_id {
3540            item.view.set_leader_peer_id(None, cx);
3541        }
3542
3543        let project_id = self.project.read(cx).remote_id();
3544        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3545        self.app_state
3546            .client
3547            .send(proto::Unfollow {
3548                room_id,
3549                project_id,
3550                leader_id: Some(leader_id),
3551            })
3552            .log_err();
3553
3554        Some(())
3555    }
3556
3557    pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3558        self.follower_states.contains_key(&peer_id)
3559    }
3560
3561    fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
3562        cx.emit(Event::ActiveItemChanged);
3563        let active_entry = self.active_project_path(cx);
3564        self.project
3565            .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3566
3567        self.update_window_title(cx);
3568    }
3569
3570    fn update_window_title(&mut self, cx: &mut WindowContext) {
3571        let project = self.project().read(cx);
3572        let mut title = String::new();
3573
3574        for (i, name) in project.worktree_root_names(cx).enumerate() {
3575            if i > 0 {
3576                title.push_str(", ");
3577            }
3578            title.push_str(name);
3579        }
3580
3581        if title.is_empty() {
3582            title = "empty project".to_string();
3583        }
3584
3585        if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3586            let filename = path
3587                .path
3588                .file_name()
3589                .map(|s| s.to_string_lossy())
3590                .or_else(|| {
3591                    Some(Cow::Borrowed(
3592                        project
3593                            .worktree_for_id(path.worktree_id, cx)?
3594                            .read(cx)
3595                            .root_name(),
3596                    ))
3597                });
3598
3599            if let Some(filename) = filename {
3600                title.push_str("");
3601                title.push_str(filename.as_ref());
3602            }
3603        }
3604
3605        if project.is_via_collab() {
3606            title.push_str("");
3607        } else if project.is_shared() {
3608            title.push_str("");
3609        }
3610
3611        cx.set_window_title(&title);
3612    }
3613
3614    fn update_window_edited(&mut self, cx: &mut WindowContext) {
3615        let is_edited = !self.project.read(cx).is_disconnected(cx)
3616            && self
3617                .items(cx)
3618                .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
3619        if is_edited != self.window_edited {
3620            self.window_edited = is_edited;
3621            cx.set_window_edited(self.window_edited)
3622        }
3623    }
3624
3625    fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
3626        if self.notifications.is_empty() {
3627            None
3628        } else {
3629            Some(
3630                div()
3631                    .absolute()
3632                    .right_3()
3633                    .bottom_3()
3634                    .w_112()
3635                    .h_full()
3636                    .flex()
3637                    .flex_col()
3638                    .justify_end()
3639                    .gap_2()
3640                    .children(
3641                        self.notifications
3642                            .iter()
3643                            .map(|(_, notification)| notification.to_any()),
3644                    ),
3645            )
3646        }
3647    }
3648
3649    // RPC handlers
3650
3651    fn active_view_for_follower(
3652        &self,
3653        follower_project_id: Option<u64>,
3654        cx: &mut ViewContext<Self>,
3655    ) -> Option<proto::View> {
3656        let (item, panel_id) = self.active_item_for_followers(cx);
3657        let item = item?;
3658        let leader_id = self
3659            .pane_for(&*item)
3660            .and_then(|pane| self.leader_for_pane(&pane));
3661
3662        let item_handle = item.to_followable_item_handle(cx)?;
3663        let id = item_handle.remote_id(&self.app_state.client, cx)?;
3664        let variant = item_handle.to_state_proto(cx)?;
3665
3666        if item_handle.is_project_item(cx)
3667            && (follower_project_id.is_none()
3668                || follower_project_id != self.project.read(cx).remote_id())
3669        {
3670            return None;
3671        }
3672
3673        Some(proto::View {
3674            id: Some(id.to_proto()),
3675            leader_id,
3676            variant: Some(variant),
3677            panel_id: panel_id.map(|id| id as i32),
3678        })
3679    }
3680
3681    fn handle_follow(
3682        &mut self,
3683        follower_project_id: Option<u64>,
3684        cx: &mut ViewContext<Self>,
3685    ) -> proto::FollowResponse {
3686        let active_view = self.active_view_for_follower(follower_project_id, cx);
3687
3688        cx.notify();
3689        proto::FollowResponse {
3690            // TODO: Remove after version 0.145.x stabilizes.
3691            active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3692            views: active_view.iter().cloned().collect(),
3693            active_view,
3694        }
3695    }
3696
3697    fn handle_update_followers(
3698        &mut self,
3699        leader_id: PeerId,
3700        message: proto::UpdateFollowers,
3701        _cx: &mut ViewContext<Self>,
3702    ) {
3703        self.leader_updates_tx
3704            .unbounded_send((leader_id, message))
3705            .ok();
3706    }
3707
3708    async fn process_leader_update(
3709        this: &WeakView<Self>,
3710        leader_id: PeerId,
3711        update: proto::UpdateFollowers,
3712        cx: &mut AsyncWindowContext,
3713    ) -> Result<()> {
3714        match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3715            proto::update_followers::Variant::CreateView(view) => {
3716                let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3717                let should_add_view = this.update(cx, |this, _| {
3718                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
3719                        anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
3720                    } else {
3721                        anyhow::Ok(false)
3722                    }
3723                })??;
3724
3725                if should_add_view {
3726                    Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3727                }
3728            }
3729            proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3730                let should_add_view = this.update(cx, |this, _| {
3731                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
3732                        state.active_view_id = update_active_view
3733                            .view
3734                            .as_ref()
3735                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3736
3737                        if state.active_view_id.is_some_and(|view_id| {
3738                            !state.items_by_leader_view_id.contains_key(&view_id)
3739                        }) {
3740                            anyhow::Ok(true)
3741                        } else {
3742                            anyhow::Ok(false)
3743                        }
3744                    } else {
3745                        anyhow::Ok(false)
3746                    }
3747                })??;
3748
3749                if should_add_view {
3750                    if let Some(view) = update_active_view.view {
3751                        Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3752                    }
3753                }
3754            }
3755            proto::update_followers::Variant::UpdateView(update_view) => {
3756                let variant = update_view
3757                    .variant
3758                    .ok_or_else(|| anyhow!("missing update view variant"))?;
3759                let id = update_view
3760                    .id
3761                    .ok_or_else(|| anyhow!("missing update view id"))?;
3762                let mut tasks = Vec::new();
3763                this.update(cx, |this, cx| {
3764                    let project = this.project.clone();
3765                    if let Some(state) = this.follower_states.get(&leader_id) {
3766                        let view_id = ViewId::from_proto(id.clone())?;
3767                        if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
3768                            tasks.push(item.view.apply_update_proto(&project, variant.clone(), cx));
3769                        }
3770                    }
3771                    anyhow::Ok(())
3772                })??;
3773                try_join_all(tasks).await.log_err();
3774            }
3775        }
3776        this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
3777        Ok(())
3778    }
3779
3780    async fn add_view_from_leader(
3781        this: WeakView<Self>,
3782        leader_id: PeerId,
3783        view: &proto::View,
3784        cx: &mut AsyncWindowContext,
3785    ) -> Result<()> {
3786        let this = this.upgrade().context("workspace dropped")?;
3787
3788        let Some(id) = view.id.clone() else {
3789            return Err(anyhow!("no id for view"));
3790        };
3791        let id = ViewId::from_proto(id)?;
3792        let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
3793
3794        let pane = this.update(cx, |this, _cx| {
3795            let state = this
3796                .follower_states
3797                .get(&leader_id)
3798                .context("stopped following")?;
3799            anyhow::Ok(state.pane().clone())
3800        })??;
3801        let existing_item = pane.update(cx, |pane, cx| {
3802            let client = this.read(cx).client().clone();
3803            pane.items().find_map(|item| {
3804                let item = item.to_followable_item_handle(cx)?;
3805                if item.remote_id(&client, cx) == Some(id) {
3806                    Some(item)
3807                } else {
3808                    None
3809                }
3810            })
3811        })?;
3812        let item = if let Some(existing_item) = existing_item {
3813            existing_item
3814        } else {
3815            let variant = view.variant.clone();
3816            if variant.is_none() {
3817                Err(anyhow!("missing view variant"))?;
3818            }
3819
3820            let task = cx.update(|cx| {
3821                FollowableViewRegistry::from_state_proto(this.clone(), id, variant, cx)
3822            })?;
3823
3824            let Some(task) = task else {
3825                return Err(anyhow!(
3826                    "failed to construct view from leader (maybe from a different version of zed?)"
3827                ));
3828            };
3829
3830            let mut new_item = task.await?;
3831            pane.update(cx, |pane, cx| {
3832                let mut item_to_remove = None;
3833                for (ix, item) in pane.items().enumerate() {
3834                    if let Some(item) = item.to_followable_item_handle(cx) {
3835                        match new_item.dedup(item.as_ref(), cx) {
3836                            Some(item::Dedup::KeepExisting) => {
3837                                new_item =
3838                                    item.boxed_clone().to_followable_item_handle(cx).unwrap();
3839                                break;
3840                            }
3841                            Some(item::Dedup::ReplaceExisting) => {
3842                                item_to_remove = Some((ix, item.item_id()));
3843                                break;
3844                            }
3845                            None => {}
3846                        }
3847                    }
3848                }
3849
3850                if let Some((ix, id)) = item_to_remove {
3851                    pane.remove_item(id, false, false, cx);
3852                    pane.add_item(new_item.boxed_clone(), false, false, Some(ix), cx);
3853                }
3854            })?;
3855
3856            new_item
3857        };
3858
3859        this.update(cx, |this, cx| {
3860            let state = this.follower_states.get_mut(&leader_id)?;
3861            item.set_leader_peer_id(Some(leader_id), cx);
3862            state.items_by_leader_view_id.insert(
3863                id,
3864                FollowerView {
3865                    view: item,
3866                    location: panel_id,
3867                },
3868            );
3869
3870            Some(())
3871        })?;
3872
3873        Ok(())
3874    }
3875
3876    pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3877        let mut is_project_item = true;
3878        let mut update = proto::UpdateActiveView::default();
3879        if cx.is_window_active() {
3880            let (active_item, panel_id) = self.active_item_for_followers(cx);
3881
3882            if let Some(item) = active_item {
3883                if item.focus_handle(cx).contains_focused(cx) {
3884                    let leader_id = self
3885                        .pane_for(&*item)
3886                        .and_then(|pane| self.leader_for_pane(&pane));
3887
3888                    if let Some(item) = item.to_followable_item_handle(cx) {
3889                        let id = item
3890                            .remote_id(&self.app_state.client, cx)
3891                            .map(|id| id.to_proto());
3892
3893                        if let Some(id) = id.clone() {
3894                            if let Some(variant) = item.to_state_proto(cx) {
3895                                let view = Some(proto::View {
3896                                    id: Some(id.clone()),
3897                                    leader_id,
3898                                    variant: Some(variant),
3899                                    panel_id: panel_id.map(|id| id as i32),
3900                                });
3901
3902                                is_project_item = item.is_project_item(cx);
3903                                update = proto::UpdateActiveView {
3904                                    view,
3905                                    // TODO: Remove after version 0.145.x stabilizes.
3906                                    id: Some(id.clone()),
3907                                    leader_id,
3908                                };
3909                            }
3910                        };
3911                    }
3912                }
3913            }
3914        }
3915
3916        let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
3917        if active_view_id != self.last_active_view_id.as_ref() {
3918            self.last_active_view_id = active_view_id.cloned();
3919            self.update_followers(
3920                is_project_item,
3921                proto::update_followers::Variant::UpdateActiveView(update),
3922                cx,
3923            );
3924        }
3925    }
3926
3927    fn active_item_for_followers(
3928        &self,
3929        cx: &mut WindowContext,
3930    ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
3931        let mut active_item = None;
3932        let mut panel_id = None;
3933        for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3934            if dock.focus_handle(cx).contains_focused(cx) {
3935                if let Some(panel) = dock.read(cx).active_panel() {
3936                    if let Some(pane) = panel.pane(cx) {
3937                        if let Some(item) = pane.read(cx).active_item() {
3938                            active_item = Some(item);
3939                            panel_id = panel.remote_id();
3940                            break;
3941                        }
3942                    }
3943                }
3944            }
3945        }
3946
3947        if active_item.is_none() {
3948            active_item = self.active_pane().read(cx).active_item();
3949        }
3950        (active_item, panel_id)
3951    }
3952
3953    fn update_followers(
3954        &self,
3955        project_only: bool,
3956        update: proto::update_followers::Variant,
3957        cx: &mut WindowContext,
3958    ) -> Option<()> {
3959        // If this update only applies to for followers in the current project,
3960        // then skip it unless this project is shared. If it applies to all
3961        // followers, regardless of project, then set `project_id` to none,
3962        // indicating that it goes to all followers.
3963        let project_id = if project_only {
3964            Some(self.project.read(cx).remote_id()?)
3965        } else {
3966            None
3967        };
3968        self.app_state().workspace_store.update(cx, |store, cx| {
3969            store.update_followers(project_id, update, cx)
3970        })
3971    }
3972
3973    pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3974        self.follower_states.iter().find_map(|(leader_id, state)| {
3975            if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
3976                Some(*leader_id)
3977            } else {
3978                None
3979            }
3980        })
3981    }
3982
3983    fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3984        cx.notify();
3985
3986        let call = self.active_call()?;
3987        let room = call.read(cx).room()?.read(cx);
3988        let participant = room.remote_participant_for_peer_id(leader_id)?;
3989
3990        let leader_in_this_app;
3991        let leader_in_this_project;
3992        match participant.location {
3993            call::ParticipantLocation::SharedProject { project_id } => {
3994                leader_in_this_app = true;
3995                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3996            }
3997            call::ParticipantLocation::UnsharedProject => {
3998                leader_in_this_app = true;
3999                leader_in_this_project = false;
4000            }
4001            call::ParticipantLocation::External => {
4002                leader_in_this_app = false;
4003                leader_in_this_project = false;
4004            }
4005        };
4006
4007        let state = self.follower_states.get(&leader_id)?;
4008        let mut item_to_activate = None;
4009        if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
4010            if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
4011                if leader_in_this_project || !item.view.is_project_item(cx) {
4012                    item_to_activate = Some((item.location, item.view.boxed_clone()));
4013                }
4014            }
4015        } else if let Some(shared_screen) =
4016            self.shared_screen_for_peer(leader_id, &state.center_pane, cx)
4017        {
4018            item_to_activate = Some((None, Box::new(shared_screen)));
4019        }
4020
4021        let (panel_id, item) = item_to_activate?;
4022
4023        let mut transfer_focus = state.center_pane.read(cx).has_focus(cx);
4024        let pane;
4025        if let Some(panel_id) = panel_id {
4026            pane = self.activate_panel_for_proto_id(panel_id, cx)?.pane(cx)?;
4027            let state = self.follower_states.get_mut(&leader_id)?;
4028            state.dock_pane = Some(pane.clone());
4029        } else {
4030            pane = state.center_pane.clone();
4031            let state = self.follower_states.get_mut(&leader_id)?;
4032            if let Some(dock_pane) = state.dock_pane.take() {
4033                transfer_focus |= dock_pane.focus_handle(cx).contains_focused(cx);
4034            }
4035        }
4036
4037        pane.update(cx, |pane, cx| {
4038            let focus_active_item = pane.has_focus(cx) || transfer_focus;
4039            if let Some(index) = pane.index_for_item(item.as_ref()) {
4040                pane.activate_item(index, false, false, cx);
4041            } else {
4042                pane.add_item(item.boxed_clone(), false, false, None, cx)
4043            }
4044
4045            if focus_active_item {
4046                pane.focus_active_item(cx)
4047            }
4048        });
4049
4050        None
4051    }
4052
4053    #[cfg(target_os = "windows")]
4054    fn shared_screen_for_peer(
4055        &self,
4056        _peer_id: PeerId,
4057        _pane: &View<Pane>,
4058        _cx: &mut WindowContext,
4059    ) -> Option<View<SharedScreen>> {
4060        None
4061    }
4062
4063    #[cfg(not(target_os = "windows"))]
4064    fn shared_screen_for_peer(
4065        &self,
4066        peer_id: PeerId,
4067        pane: &View<Pane>,
4068        cx: &mut WindowContext,
4069    ) -> Option<View<SharedScreen>> {
4070        let call = self.active_call()?;
4071        let room = call.read(cx).room()?.read(cx);
4072        let participant = room.remote_participant_for_peer_id(peer_id)?;
4073        let track = participant.video_tracks.values().next()?.clone();
4074        let user = participant.user.clone();
4075
4076        for item in pane.read(cx).items_of_type::<SharedScreen>() {
4077            if item.read(cx).peer_id == peer_id {
4078                return Some(item);
4079            }
4080        }
4081
4082        Some(cx.new_view(|cx| SharedScreen::new(track, peer_id, user.clone(), cx)))
4083    }
4084
4085    pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
4086        if cx.is_window_active() {
4087            self.update_active_view_for_followers(cx);
4088
4089            if let Some(database_id) = self.database_id {
4090                cx.background_executor()
4091                    .spawn(persistence::DB.update_timestamp(database_id))
4092                    .detach();
4093            }
4094        } else {
4095            for pane in &self.panes {
4096                pane.update(cx, |pane, cx| {
4097                    if let Some(item) = pane.active_item() {
4098                        item.workspace_deactivated(cx);
4099                    }
4100                    for item in pane.items() {
4101                        if matches!(
4102                            item.workspace_settings(cx).autosave,
4103                            AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4104                        ) {
4105                            Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
4106                                .detach_and_log_err(cx);
4107                        }
4108                    }
4109                });
4110            }
4111        }
4112    }
4113
4114    pub fn active_call(&self) -> Option<&Model<ActiveCall>> {
4115        self.active_call.as_ref().map(|(call, _)| call)
4116    }
4117
4118    fn on_active_call_event(
4119        &mut self,
4120        _: Model<ActiveCall>,
4121        event: &call::room::Event,
4122        cx: &mut ViewContext<Self>,
4123    ) {
4124        match event {
4125            call::room::Event::ParticipantLocationChanged { participant_id }
4126            | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4127                self.leader_updated(*participant_id, cx);
4128            }
4129            _ => {}
4130        }
4131    }
4132
4133    pub fn database_id(&self) -> Option<WorkspaceId> {
4134        self.database_id
4135    }
4136
4137    fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
4138        let project = self.project().read(cx);
4139
4140        if project.is_local() {
4141            Some(
4142                project
4143                    .visible_worktrees(cx)
4144                    .map(|worktree| worktree.read(cx).abs_path())
4145                    .collect::<Vec<_>>(),
4146            )
4147        } else {
4148            None
4149        }
4150    }
4151
4152    fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
4153        match member {
4154            Member::Axis(PaneAxis { members, .. }) => {
4155                for child in members.iter() {
4156                    self.remove_panes(child.clone(), cx)
4157                }
4158            }
4159            Member::Pane(pane) => {
4160                self.force_remove_pane(&pane, &None, cx);
4161            }
4162        }
4163    }
4164
4165    fn remove_from_session(&mut self, cx: &mut WindowContext) -> Task<()> {
4166        self.session_id.take();
4167        self.serialize_workspace_internal(cx)
4168    }
4169
4170    fn force_remove_pane(
4171        &mut self,
4172        pane: &View<Pane>,
4173        focus_on: &Option<View<Pane>>,
4174        cx: &mut ViewContext<Workspace>,
4175    ) {
4176        self.panes.retain(|p| p != pane);
4177        if let Some(focus_on) = focus_on {
4178            focus_on.update(cx, |pane, cx| pane.focus(cx));
4179        } else {
4180            self.panes
4181                .last()
4182                .unwrap()
4183                .update(cx, |pane, cx| pane.focus(cx));
4184        }
4185        if self.last_active_center_pane == Some(pane.downgrade()) {
4186            self.last_active_center_pane = None;
4187        }
4188        cx.notify();
4189    }
4190
4191    fn serialize_workspace(&mut self, cx: &mut ViewContext<Self>) {
4192        if self._schedule_serialize.is_none() {
4193            self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
4194                cx.background_executor()
4195                    .timer(Duration::from_millis(100))
4196                    .await;
4197                this.update(&mut cx, |this, cx| {
4198                    this.serialize_workspace_internal(cx).detach();
4199                    this._schedule_serialize.take();
4200                })
4201                .log_err();
4202            }));
4203        }
4204    }
4205
4206    fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
4207        let Some(database_id) = self.database_id() else {
4208            return Task::ready(());
4209        };
4210
4211        fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
4212            let (items, active, pinned_count) = {
4213                let pane = pane_handle.read(cx);
4214                let active_item_id = pane.active_item().map(|item| item.item_id());
4215                (
4216                    pane.items()
4217                        .filter_map(|handle| {
4218                            let handle = handle.to_serializable_item_handle(cx)?;
4219
4220                            Some(SerializedItem {
4221                                kind: Arc::from(handle.serialized_item_kind()),
4222                                item_id: handle.item_id().as_u64(),
4223                                active: Some(handle.item_id()) == active_item_id,
4224                                preview: pane.is_active_preview_item(handle.item_id()),
4225                            })
4226                        })
4227                        .collect::<Vec<_>>(),
4228                    pane.has_focus(cx),
4229                    pane.pinned_count(),
4230                )
4231            };
4232
4233            SerializedPane::new(items, active, pinned_count)
4234        }
4235
4236        fn build_serialized_pane_group(
4237            pane_group: &Member,
4238            cx: &WindowContext,
4239        ) -> SerializedPaneGroup {
4240            match pane_group {
4241                Member::Axis(PaneAxis {
4242                    axis,
4243                    members,
4244                    flexes,
4245                    bounding_boxes: _,
4246                }) => SerializedPaneGroup::Group {
4247                    axis: SerializedAxis(*axis),
4248                    children: members
4249                        .iter()
4250                        .map(|member| build_serialized_pane_group(member, cx))
4251                        .collect::<Vec<_>>(),
4252                    flexes: Some(flexes.lock().clone()),
4253                },
4254                Member::Pane(pane_handle) => {
4255                    SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
4256                }
4257            }
4258        }
4259
4260        fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
4261            let left_dock = this.left_dock.read(cx);
4262            let left_visible = left_dock.is_open();
4263            let left_active_panel = left_dock
4264                .active_panel()
4265                .map(|panel| panel.persistent_name().to_string());
4266            let left_dock_zoom = left_dock
4267                .active_panel()
4268                .map(|panel| panel.is_zoomed(cx))
4269                .unwrap_or(false);
4270
4271            let right_dock = this.right_dock.read(cx);
4272            let right_visible = right_dock.is_open();
4273            let right_active_panel = right_dock
4274                .active_panel()
4275                .map(|panel| panel.persistent_name().to_string());
4276            let right_dock_zoom = right_dock
4277                .active_panel()
4278                .map(|panel| panel.is_zoomed(cx))
4279                .unwrap_or(false);
4280
4281            let bottom_dock = this.bottom_dock.read(cx);
4282            let bottom_visible = bottom_dock.is_open();
4283            let bottom_active_panel = bottom_dock
4284                .active_panel()
4285                .map(|panel| panel.persistent_name().to_string());
4286            let bottom_dock_zoom = bottom_dock
4287                .active_panel()
4288                .map(|panel| panel.is_zoomed(cx))
4289                .unwrap_or(false);
4290
4291            DockStructure {
4292                left: DockData {
4293                    visible: left_visible,
4294                    active_panel: left_active_panel,
4295                    zoom: left_dock_zoom,
4296                },
4297                right: DockData {
4298                    visible: right_visible,
4299                    active_panel: right_active_panel,
4300                    zoom: right_dock_zoom,
4301                },
4302                bottom: DockData {
4303                    visible: bottom_visible,
4304                    active_panel: bottom_active_panel,
4305                    zoom: bottom_dock_zoom,
4306                },
4307            }
4308        }
4309
4310        let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4311            Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4312        } else if let Some(local_paths) = self.local_paths(cx) {
4313            if !local_paths.is_empty() {
4314                Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4315            } else {
4316                None
4317            }
4318        } else {
4319            None
4320        };
4321
4322        if let Some(location) = location {
4323            let center_group = build_serialized_pane_group(&self.center.root, cx);
4324            let docks = build_serialized_docks(self, cx);
4325            let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
4326            let serialized_workspace = SerializedWorkspace {
4327                id: database_id,
4328                location,
4329                center_group,
4330                window_bounds,
4331                display: Default::default(),
4332                docks,
4333                centered_layout: self.centered_layout,
4334                session_id: self.session_id.clone(),
4335                window_id: Some(cx.window_handle().window_id().as_u64()),
4336            };
4337            return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
4338        }
4339        Task::ready(())
4340    }
4341
4342    async fn serialize_items(
4343        this: &WeakView<Self>,
4344        items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4345        cx: &mut AsyncWindowContext,
4346    ) -> Result<()> {
4347        const CHUNK_SIZE: usize = 200;
4348        const THROTTLE_TIME: Duration = Duration::from_millis(200);
4349
4350        let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4351
4352        while let Some(items_received) = serializable_items.next().await {
4353            let unique_items =
4354                items_received
4355                    .into_iter()
4356                    .fold(HashMap::default(), |mut acc, item| {
4357                        acc.entry(item.item_id()).or_insert(item);
4358                        acc
4359                    });
4360
4361            // We use into_iter() here so that the references to the items are moved into
4362            // the tasks and not kept alive while we're sleeping.
4363            for (_, item) in unique_items.into_iter() {
4364                if let Ok(Some(task)) =
4365                    this.update(cx, |workspace, cx| item.serialize(workspace, false, cx))
4366                {
4367                    cx.background_executor()
4368                        .spawn(async move { task.await.log_err() })
4369                        .detach();
4370                }
4371            }
4372
4373            cx.background_executor().timer(THROTTLE_TIME).await;
4374        }
4375
4376        Ok(())
4377    }
4378
4379    pub(crate) fn enqueue_item_serialization(
4380        &mut self,
4381        item: Box<dyn SerializableItemHandle>,
4382    ) -> Result<()> {
4383        self.serializable_items_tx
4384            .unbounded_send(item)
4385            .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4386    }
4387
4388    pub(crate) fn load_workspace(
4389        serialized_workspace: SerializedWorkspace,
4390        paths_to_open: Vec<Option<ProjectPath>>,
4391        cx: &mut ViewContext<Workspace>,
4392    ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4393        cx.spawn(|workspace, mut cx| async move {
4394            let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4395
4396            let mut center_group = None;
4397            let mut center_items = None;
4398
4399            // Traverse the splits tree and add to things
4400            if let Some((group, active_pane, items)) = serialized_workspace
4401                .center_group
4402                .deserialize(
4403                    &project,
4404                    serialized_workspace.id,
4405                    workspace.clone(),
4406                    &mut cx,
4407                )
4408                .await
4409            {
4410                center_items = Some(items);
4411                center_group = Some((group, active_pane))
4412            }
4413
4414            let mut items_by_project_path = HashMap::default();
4415            let mut item_ids_by_kind = HashMap::default();
4416            let mut all_deserialized_items = Vec::default();
4417            cx.update(|cx| {
4418                for item in center_items.unwrap_or_default().into_iter().flatten() {
4419                    if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4420                        item_ids_by_kind
4421                            .entry(serializable_item_handle.serialized_item_kind())
4422                            .or_insert(Vec::new())
4423                            .push(item.item_id().as_u64() as ItemId);
4424                    }
4425
4426                    if let Some(project_path) = item.project_path(cx) {
4427                        items_by_project_path.insert(project_path, item.clone());
4428                    }
4429                    all_deserialized_items.push(item);
4430                }
4431            })?;
4432
4433            let opened_items = paths_to_open
4434                .into_iter()
4435                .map(|path_to_open| {
4436                    path_to_open
4437                        .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4438                })
4439                .collect::<Vec<_>>();
4440
4441            // Remove old panes from workspace panes list
4442            workspace.update(&mut cx, |workspace, cx| {
4443                if let Some((center_group, active_pane)) = center_group {
4444                    workspace.remove_panes(workspace.center.root.clone(), cx);
4445
4446                    // Swap workspace center group
4447                    workspace.center = PaneGroup::with_root(center_group);
4448                    if let Some(active_pane) = active_pane {
4449                        workspace.set_active_pane(&active_pane, cx);
4450                        cx.focus_self();
4451                    } else {
4452                        workspace.set_active_pane(&workspace.center.first_pane(), cx);
4453                    }
4454                }
4455
4456                let docks = serialized_workspace.docks;
4457
4458                for (dock, serialized_dock) in [
4459                    (&mut workspace.right_dock, docks.right),
4460                    (&mut workspace.left_dock, docks.left),
4461                    (&mut workspace.bottom_dock, docks.bottom),
4462                ]
4463                .iter_mut()
4464                {
4465                    dock.update(cx, |dock, cx| {
4466                        dock.serialized_dock = Some(serialized_dock.clone());
4467                        dock.restore_state(cx);
4468                    });
4469                }
4470
4471                cx.notify();
4472            })?;
4473
4474            // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4475            // after loading the items, we might have different items and in order to avoid
4476            // the database filling up, we delete items that haven't been loaded now.
4477            //
4478            // The items that have been loaded, have been saved after they've been added to the workspace.
4479            let clean_up_tasks = workspace.update(&mut cx, |_, cx| {
4480                item_ids_by_kind
4481                    .into_iter()
4482                    .map(|(item_kind, loaded_items)| {
4483                        SerializableItemRegistry::cleanup(
4484                            item_kind,
4485                            serialized_workspace.id,
4486                            loaded_items,
4487                            cx,
4488                        )
4489                        .log_err()
4490                    })
4491                    .collect::<Vec<_>>()
4492            })?;
4493
4494            futures::future::join_all(clean_up_tasks).await;
4495
4496            workspace
4497                .update(&mut cx, |workspace, cx| {
4498                    // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4499                    workspace.serialize_workspace_internal(cx).detach();
4500
4501                    // Ensure that we mark the window as edited if we did load dirty items
4502                    workspace.update_window_edited(cx);
4503                })
4504                .ok();
4505
4506            Ok(opened_items)
4507        })
4508    }
4509
4510    fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
4511        self.add_workspace_actions_listeners(div, cx)
4512            .on_action(cx.listener(Self::close_inactive_items_and_panes))
4513            .on_action(cx.listener(Self::close_all_items_and_panes))
4514            .on_action(cx.listener(Self::save_all))
4515            .on_action(cx.listener(Self::send_keystrokes))
4516            .on_action(cx.listener(Self::add_folder_to_project))
4517            .on_action(cx.listener(Self::follow_next_collaborator))
4518            .on_action(cx.listener(Self::close_window))
4519            .on_action(cx.listener(Self::activate_pane_at_index))
4520            .on_action(cx.listener(Self::move_item_to_pane_at_index))
4521            .on_action(cx.listener(Self::move_focused_panel_to_next_position))
4522            .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
4523                let pane = workspace.active_pane().clone();
4524                workspace.unfollow_in_pane(&pane, cx);
4525            }))
4526            .on_action(cx.listener(|workspace, action: &Save, cx| {
4527                workspace
4528                    .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
4529                    .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4530            }))
4531            .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
4532                workspace
4533                    .save_active_item(SaveIntent::SaveWithoutFormat, cx)
4534                    .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4535            }))
4536            .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
4537                workspace
4538                    .save_active_item(SaveIntent::SaveAs, cx)
4539                    .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4540            }))
4541            .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
4542                workspace.activate_previous_pane(cx)
4543            }))
4544            .on_action(
4545                cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
4546            )
4547            .on_action(cx.listener(|workspace, _: &ActivateNextWindow, cx| {
4548                workspace.activate_next_window(cx)
4549            }))
4550            .on_action(cx.listener(|workspace, _: &ActivatePreviousWindow, cx| {
4551                workspace.activate_previous_window(cx)
4552            }))
4553            .on_action(
4554                cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
4555                    workspace.activate_pane_in_direction(action.0, cx)
4556                }),
4557            )
4558            .on_action(
4559                cx.listener(|workspace, action: &MoveItemToPaneInDirection, cx| {
4560                    workspace.move_item_to_pane_in_direction(action, cx)
4561                }),
4562            )
4563            .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
4564                workspace.swap_pane_in_direction(action.0, cx)
4565            }))
4566            .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
4567                this.toggle_dock(DockPosition::Left, cx);
4568            }))
4569            .on_action(
4570                cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
4571                    workspace.toggle_dock(DockPosition::Right, cx);
4572                }),
4573            )
4574            .on_action(
4575                cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
4576                    workspace.toggle_dock(DockPosition::Bottom, cx);
4577                }),
4578            )
4579            .on_action(
4580                cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
4581                    workspace.close_all_docks(cx);
4582                }),
4583            )
4584            .on_action(
4585                cx.listener(|workspace: &mut Workspace, _: &ClearAllNotifications, cx| {
4586                    workspace.clear_all_notifications(cx);
4587                }),
4588            )
4589            .on_action(
4590                cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
4591                    workspace.reopen_closed_item(cx).detach();
4592                }),
4593            )
4594            .on_action(cx.listener(Workspace::toggle_centered_layout))
4595    }
4596
4597    #[cfg(any(test, feature = "test-support"))]
4598    pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
4599        use node_runtime::NodeRuntime;
4600        use session::Session;
4601
4602        let client = project.read(cx).client();
4603        let user_store = project.read(cx).user_store();
4604
4605        let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
4606        let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
4607        cx.activate_window();
4608        let app_state = Arc::new(AppState {
4609            languages: project.read(cx).languages().clone(),
4610            workspace_store,
4611            client,
4612            user_store,
4613            fs: project.read(cx).fs().clone(),
4614            build_window_options: |_, _| Default::default(),
4615            node_runtime: NodeRuntime::unavailable(),
4616            session,
4617        });
4618        let workspace = Self::new(Default::default(), project, app_state, cx);
4619        workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
4620        workspace
4621    }
4622
4623    pub fn register_action<A: Action>(
4624        &mut self,
4625        callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
4626    ) -> &mut Self {
4627        let callback = Arc::new(callback);
4628
4629        self.workspace_actions.push(Box::new(move |div, cx| {
4630            let callback = callback.clone();
4631            div.on_action(
4632                cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
4633            )
4634        }));
4635        self
4636    }
4637
4638    fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
4639        for action in self.workspace_actions.iter() {
4640            div = (action)(div, cx)
4641        }
4642        div
4643    }
4644
4645    pub fn has_active_modal(&self, cx: &WindowContext) -> bool {
4646        self.modal_layer.read(cx).has_active_modal()
4647    }
4648
4649    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &AppContext) -> Option<View<V>> {
4650        self.modal_layer.read(cx).active_modal()
4651    }
4652
4653    pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
4654    where
4655        B: FnOnce(&mut ViewContext<V>) -> V,
4656    {
4657        self.modal_layer
4658            .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
4659    }
4660
4661    pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
4662        self.centered_layout = !self.centered_layout;
4663        if let Some(database_id) = self.database_id() {
4664            cx.background_executor()
4665                .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4666                .detach_and_log_err(cx);
4667        }
4668        cx.notify();
4669    }
4670
4671    fn adjust_padding(padding: Option<f32>) -> f32 {
4672        padding
4673            .unwrap_or(Self::DEFAULT_PADDING)
4674            .clamp(0.0, Self::MAX_PADDING)
4675    }
4676
4677    fn render_dock(
4678        &self,
4679        position: DockPosition,
4680        dock: &View<Dock>,
4681        cx: &WindowContext,
4682    ) -> Option<Div> {
4683        if self.zoomed_position == Some(position) {
4684            return None;
4685        }
4686
4687        let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4688            let pane = panel.pane(cx)?;
4689            let follower_states = &self.follower_states;
4690            leader_border_for_pane(follower_states, &pane, cx)
4691        });
4692
4693        Some(
4694            div()
4695                .flex()
4696                .flex_none()
4697                .overflow_hidden()
4698                .child(dock.clone())
4699                .children(leader_border),
4700        )
4701    }
4702
4703    pub fn for_window(cx: &mut WindowContext) -> Option<View<Workspace>> {
4704        let window = cx.window_handle().downcast::<Workspace>()?;
4705        cx.read_window(&window, |workspace, _| workspace).ok()
4706    }
4707
4708    pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
4709        self.zoomed.as_ref()
4710    }
4711
4712    pub fn activate_next_window(&mut self, cx: &mut ViewContext<Self>) {
4713        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
4714            return;
4715        };
4716        let windows = cx.windows();
4717        let Some(next_window) = windows
4718            .iter()
4719            .cycle()
4720            .skip_while(|window| window.window_id() != current_window_id)
4721            .nth(1)
4722        else {
4723            return;
4724        };
4725        next_window.update(cx, |_, cx| cx.activate_window()).ok();
4726    }
4727
4728    pub fn activate_previous_window(&mut self, cx: &mut ViewContext<Self>) {
4729        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
4730            return;
4731        };
4732        let windows = cx.windows();
4733        let Some(prev_window) = windows
4734            .iter()
4735            .rev()
4736            .cycle()
4737            .skip_while(|window| window.window_id() != current_window_id)
4738            .nth(1)
4739        else {
4740            return;
4741        };
4742        prev_window.update(cx, |_, cx| cx.activate_window()).ok();
4743    }
4744}
4745
4746fn leader_border_for_pane(
4747    follower_states: &HashMap<PeerId, FollowerState>,
4748    pane: &View<Pane>,
4749    cx: &WindowContext,
4750) -> Option<Div> {
4751    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
4752        if state.pane() == pane {
4753            Some((*leader_id, state))
4754        } else {
4755            None
4756        }
4757    })?;
4758
4759    let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
4760    let leader = room.remote_participant_for_peer_id(leader_id)?;
4761
4762    let mut leader_color = cx
4763        .theme()
4764        .players()
4765        .color_for_participant(leader.participant_index.0)
4766        .cursor;
4767    leader_color.fade_out(0.3);
4768    Some(
4769        div()
4770            .absolute()
4771            .size_full()
4772            .left_0()
4773            .top_0()
4774            .border_2()
4775            .border_color(leader_color),
4776    )
4777}
4778
4779fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
4780    ZED_WINDOW_POSITION
4781        .zip(*ZED_WINDOW_SIZE)
4782        .map(|(position, size)| Bounds {
4783            origin: position,
4784            size,
4785        })
4786}
4787
4788fn open_items(
4789    serialized_workspace: Option<SerializedWorkspace>,
4790    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
4791    cx: &mut ViewContext<Workspace>,
4792) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
4793    let restored_items = serialized_workspace.map(|serialized_workspace| {
4794        Workspace::load_workspace(
4795            serialized_workspace,
4796            project_paths_to_open
4797                .iter()
4798                .map(|(_, project_path)| project_path)
4799                .cloned()
4800                .collect(),
4801            cx,
4802        )
4803    });
4804
4805    cx.spawn(|workspace, mut cx| async move {
4806        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
4807
4808        if let Some(restored_items) = restored_items {
4809            let restored_items = restored_items.await?;
4810
4811            let restored_project_paths = restored_items
4812                .iter()
4813                .filter_map(|item| {
4814                    cx.update(|cx| item.as_ref()?.project_path(cx))
4815                        .ok()
4816                        .flatten()
4817                })
4818                .collect::<HashSet<_>>();
4819
4820            for restored_item in restored_items {
4821                opened_items.push(restored_item.map(Ok));
4822            }
4823
4824            project_paths_to_open
4825                .iter_mut()
4826                .for_each(|(_, project_path)| {
4827                    if let Some(project_path_to_open) = project_path {
4828                        if restored_project_paths.contains(project_path_to_open) {
4829                            *project_path = None;
4830                        }
4831                    }
4832                });
4833        } else {
4834            for _ in 0..project_paths_to_open.len() {
4835                opened_items.push(None);
4836            }
4837        }
4838        assert!(opened_items.len() == project_paths_to_open.len());
4839
4840        let tasks =
4841            project_paths_to_open
4842                .into_iter()
4843                .enumerate()
4844                .map(|(ix, (abs_path, project_path))| {
4845                    let workspace = workspace.clone();
4846                    cx.spawn(|mut cx| async move {
4847                        let file_project_path = project_path?;
4848                        let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
4849                            workspace.project().update(cx, |project, cx| {
4850                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
4851                            })
4852                        });
4853
4854                        // We only want to open file paths here. If one of the items
4855                        // here is a directory, it was already opened further above
4856                        // with a `find_or_create_worktree`.
4857                        if let Ok(task) = abs_path_task {
4858                            if task.await.map_or(true, |p| p.is_file()) {
4859                                return Some((
4860                                    ix,
4861                                    workspace
4862                                        .update(&mut cx, |workspace, cx| {
4863                                            workspace.open_path(file_project_path, None, true, cx)
4864                                        })
4865                                        .log_err()?
4866                                        .await,
4867                                ));
4868                            }
4869                        }
4870                        None
4871                    })
4872                });
4873
4874        let tasks = tasks.collect::<Vec<_>>();
4875
4876        let tasks = futures::future::join_all(tasks);
4877        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4878            opened_items[ix] = Some(path_open_result);
4879        }
4880
4881        Ok(opened_items)
4882    })
4883}
4884
4885enum ActivateInDirectionTarget {
4886    Pane(View<Pane>),
4887    Dock(View<Dock>),
4888}
4889
4890fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4891    const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=admin+read%2Ctriage%2Cbug&projects=&template=1_bug_report.yml";
4892
4893    workspace
4894        .update(cx, |workspace, cx| {
4895            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4896                struct DatabaseFailedNotification;
4897
4898                workspace.show_notification_once(
4899                    NotificationId::unique::<DatabaseFailedNotification>(),
4900                    cx,
4901                    |cx| {
4902                        cx.new_view(|_| {
4903                            MessageNotification::new("Failed to load the database file.")
4904                                .with_click_message("File an issue")
4905                                .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4906                        })
4907                    },
4908                );
4909            }
4910        })
4911        .log_err();
4912}
4913
4914impl FocusableView for Workspace {
4915    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4916        self.active_pane.focus_handle(cx)
4917    }
4918}
4919
4920#[derive(Clone, Render)]
4921struct DraggedDock(DockPosition);
4922
4923impl Render for Workspace {
4924    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4925        let mut context = KeyContext::new_with_defaults();
4926        context.add("Workspace");
4927        context.set("keyboard_layout", cx.keyboard_layout().clone());
4928        let centered_layout = self.centered_layout
4929            && self.center.panes().len() == 1
4930            && self.active_item(cx).is_some();
4931        let render_padding = |size| {
4932            (size > 0.0).then(|| {
4933                div()
4934                    .h_full()
4935                    .w(relative(size))
4936                    .bg(cx.theme().colors().editor_background)
4937                    .border_color(cx.theme().colors().pane_group_border)
4938            })
4939        };
4940        let paddings = if centered_layout {
4941            let settings = WorkspaceSettings::get_global(cx).centered_layout;
4942            (
4943                render_padding(Self::adjust_padding(settings.left_padding)),
4944                render_padding(Self::adjust_padding(settings.right_padding)),
4945            )
4946        } else {
4947            (None, None)
4948        };
4949        let ui_font = theme::setup_ui_font(cx);
4950
4951        let theme = cx.theme().clone();
4952        let colors = theme.colors();
4953
4954        client_side_decorations(
4955            self.actions(div(), cx)
4956                .key_context(context)
4957                .relative()
4958                .size_full()
4959                .flex()
4960                .flex_col()
4961                .font(ui_font)
4962                .gap_0()
4963                .justify_start()
4964                .items_start()
4965                .text_color(colors.text)
4966                .overflow_hidden()
4967                .children(self.titlebar_item.clone())
4968                .child(
4969                    div()
4970                        .size_full()
4971                        .relative()
4972                        .flex_1()
4973                        .flex()
4974                        .flex_col()
4975                        .child(
4976                            div()
4977                                .id("workspace")
4978                                .bg(colors.background)
4979                                .relative()
4980                                .flex_1()
4981                                .w_full()
4982                                .flex()
4983                                .flex_col()
4984                                .overflow_hidden()
4985                                .border_t_1()
4986                                .border_b_1()
4987                                .border_color(colors.border)
4988                                .child({
4989                                    let this = cx.view().clone();
4990                                    canvas(
4991                                        move |bounds, cx| {
4992                                            this.update(cx, |this, cx| {
4993                                                let bounds_changed = this.bounds != bounds;
4994                                                this.bounds = bounds;
4995
4996                                                if bounds_changed {
4997                                                    this.left_dock.update(cx, |dock, cx| {
4998                                                        dock.clamp_panel_size(bounds.size.width, cx)
4999                                                    });
5000
5001                                                    this.right_dock.update(cx, |dock, cx| {
5002                                                        dock.clamp_panel_size(bounds.size.width, cx)
5003                                                    });
5004
5005                                                    this.bottom_dock.update(cx, |dock, cx| {
5006                                                        dock.clamp_panel_size(
5007                                                            bounds.size.height,
5008                                                            cx,
5009                                                        )
5010                                                    });
5011                                                }
5012                                            })
5013                                        },
5014                                        |_, _, _| {},
5015                                    )
5016                                    .absolute()
5017                                    .size_full()
5018                                })
5019                                .when(self.zoomed.is_none(), |this| {
5020                                    this.on_drag_move(cx.listener(
5021                                        move |workspace, e: &DragMoveEvent<DraggedDock>, cx| {
5022                                            if workspace.previous_dock_drag_coordinates
5023                                                != Some(e.event.position)
5024                                            {
5025                                                workspace.previous_dock_drag_coordinates =
5026                                                    Some(e.event.position);
5027                                                match e.drag(cx).0 {
5028                                                    DockPosition::Left => {
5029                                                        resize_left_dock(
5030                                                            e.event.position.x
5031                                                                - workspace.bounds.left(),
5032                                                            workspace,
5033                                                            cx,
5034                                                        );
5035                                                    }
5036                                                    DockPosition::Right => {
5037                                                        resize_right_dock(
5038                                                            workspace.bounds.right()
5039                                                                - e.event.position.x,
5040                                                            workspace,
5041                                                            cx,
5042                                                        );
5043                                                    }
5044                                                    DockPosition::Bottom => {
5045                                                        resize_bottom_dock(
5046                                                            workspace.bounds.bottom()
5047                                                                - e.event.position.y,
5048                                                            workspace,
5049                                                            cx,
5050                                                        );
5051                                                    }
5052                                                };
5053                                                workspace.serialize_workspace(cx);
5054                                            }
5055                                        },
5056                                    ))
5057                                })
5058                                .child(
5059                                    div()
5060                                        .flex()
5061                                        .flex_row()
5062                                        .h_full()
5063                                        // Left Dock
5064                                        .children(self.render_dock(
5065                                            DockPosition::Left,
5066                                            &self.left_dock,
5067                                            cx,
5068                                        ))
5069                                        // Panes
5070                                        .child(
5071                                            div()
5072                                                .flex()
5073                                                .flex_col()
5074                                                .flex_1()
5075                                                .overflow_hidden()
5076                                                .child(
5077                                                    h_flex()
5078                                                        .flex_1()
5079                                                        .when_some(paddings.0, |this, p| {
5080                                                            this.child(p.border_r_1())
5081                                                        })
5082                                                        .child(self.center.render(
5083                                                            &self.project,
5084                                                            &self.follower_states,
5085                                                            self.active_call(),
5086                                                            &self.active_pane,
5087                                                            self.zoomed.as_ref(),
5088                                                            &self.app_state,
5089                                                            cx,
5090                                                        ))
5091                                                        .when_some(paddings.1, |this, p| {
5092                                                            this.child(p.border_l_1())
5093                                                        }),
5094                                                )
5095                                                .children(self.render_dock(
5096                                                    DockPosition::Bottom,
5097                                                    &self.bottom_dock,
5098                                                    cx,
5099                                                )),
5100                                        )
5101                                        // Right Dock
5102                                        .children(self.render_dock(
5103                                            DockPosition::Right,
5104                                            &self.right_dock,
5105                                            cx,
5106                                        )),
5107                                )
5108                                .children(self.zoomed.as_ref().and_then(|view| {
5109                                    let zoomed_view = view.upgrade()?;
5110                                    let div = div()
5111                                        .occlude()
5112                                        .absolute()
5113                                        .overflow_hidden()
5114                                        .border_color(colors.border)
5115                                        .bg(colors.background)
5116                                        .child(zoomed_view)
5117                                        .inset_0()
5118                                        .shadow_lg();
5119
5120                                    Some(match self.zoomed_position {
5121                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
5122                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
5123                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5124                                        None => {
5125                                            div.top_2().bottom_2().left_2().right_2().border_1()
5126                                        }
5127                                    })
5128                                }))
5129                                .children(self.render_notifications(cx)),
5130                        )
5131                        .child(self.status_bar.clone())
5132                        .child(self.modal_layer.clone()),
5133                ),
5134            cx,
5135        )
5136    }
5137}
5138
5139fn resize_bottom_dock(
5140    new_size: Pixels,
5141    workspace: &mut Workspace,
5142    cx: &mut ViewContext<Workspace>,
5143) {
5144    let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5145    workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5146        bottom_dock.resize_active_panel(Some(size), cx);
5147    });
5148}
5149
5150fn resize_right_dock(new_size: Pixels, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
5151    let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5152    workspace.right_dock.update(cx, |right_dock, cx| {
5153        right_dock.resize_active_panel(Some(size), cx);
5154    });
5155}
5156
5157fn resize_left_dock(new_size: Pixels, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
5158    let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5159
5160    workspace.left_dock.update(cx, |left_dock, cx| {
5161        left_dock.resize_active_panel(Some(size), cx);
5162    });
5163}
5164
5165impl WorkspaceStore {
5166    pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
5167        Self {
5168            workspaces: Default::default(),
5169            _subscriptions: vec![
5170                client.add_request_handler(cx.weak_model(), Self::handle_follow),
5171                client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
5172            ],
5173            client,
5174        }
5175    }
5176
5177    pub fn update_followers(
5178        &self,
5179        project_id: Option<u64>,
5180        update: proto::update_followers::Variant,
5181        cx: &AppContext,
5182    ) -> Option<()> {
5183        let active_call = ActiveCall::try_global(cx)?;
5184        let room_id = active_call.read(cx).room()?.read(cx).id();
5185        self.client
5186            .send(proto::UpdateFollowers {
5187                room_id,
5188                project_id,
5189                variant: Some(update),
5190            })
5191            .log_err()
5192    }
5193
5194    pub async fn handle_follow(
5195        this: Model<Self>,
5196        envelope: TypedEnvelope<proto::Follow>,
5197        mut cx: AsyncAppContext,
5198    ) -> Result<proto::FollowResponse> {
5199        this.update(&mut cx, |this, cx| {
5200            let follower = Follower {
5201                project_id: envelope.payload.project_id,
5202                peer_id: envelope.original_sender_id()?,
5203            };
5204
5205            let mut response = proto::FollowResponse::default();
5206            this.workspaces.retain(|workspace| {
5207                workspace
5208                    .update(cx, |workspace, cx| {
5209                        let handler_response = workspace.handle_follow(follower.project_id, cx);
5210                        if let Some(active_view) = handler_response.active_view.clone() {
5211                            if workspace.project.read(cx).remote_id() == follower.project_id {
5212                                response.active_view = Some(active_view)
5213                            }
5214                        }
5215                    })
5216                    .is_ok()
5217            });
5218
5219            Ok(response)
5220        })?
5221    }
5222
5223    async fn handle_update_followers(
5224        this: Model<Self>,
5225        envelope: TypedEnvelope<proto::UpdateFollowers>,
5226        mut cx: AsyncAppContext,
5227    ) -> Result<()> {
5228        let leader_id = envelope.original_sender_id()?;
5229        let update = envelope.payload;
5230
5231        this.update(&mut cx, |this, cx| {
5232            this.workspaces.retain(|workspace| {
5233                workspace
5234                    .update(cx, |workspace, cx| {
5235                        let project_id = workspace.project.read(cx).remote_id();
5236                        if update.project_id != project_id && update.project_id.is_some() {
5237                            return;
5238                        }
5239                        workspace.handle_update_followers(leader_id, update.clone(), cx);
5240                    })
5241                    .is_ok()
5242            });
5243            Ok(())
5244        })?
5245    }
5246}
5247
5248impl ViewId {
5249    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5250        Ok(Self {
5251            creator: message
5252                .creator
5253                .ok_or_else(|| anyhow!("creator is missing"))?,
5254            id: message.id,
5255        })
5256    }
5257
5258    pub(crate) fn to_proto(self) -> proto::ViewId {
5259        proto::ViewId {
5260            creator: Some(self.creator),
5261            id: self.id,
5262        }
5263    }
5264}
5265
5266impl FollowerState {
5267    fn pane(&self) -> &View<Pane> {
5268        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5269    }
5270}
5271
5272pub trait WorkspaceHandle {
5273    fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
5274}
5275
5276impl WorkspaceHandle for View<Workspace> {
5277    fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
5278        self.read(cx)
5279            .worktrees(cx)
5280            .flat_map(|worktree| {
5281                let worktree_id = worktree.read(cx).id();
5282                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5283                    worktree_id,
5284                    path: f.path.clone(),
5285                })
5286            })
5287            .collect::<Vec<_>>()
5288    }
5289}
5290
5291impl std::fmt::Debug for OpenPaths {
5292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5293        f.debug_struct("OpenPaths")
5294            .field("paths", &self.paths)
5295            .finish()
5296    }
5297}
5298
5299pub fn activate_workspace_for_project(
5300    cx: &mut AppContext,
5301    predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
5302) -> Option<WindowHandle<Workspace>> {
5303    for window in cx.windows() {
5304        let Some(workspace) = window.downcast::<Workspace>() else {
5305            continue;
5306        };
5307
5308        let predicate = workspace
5309            .update(cx, |workspace, cx| {
5310                let project = workspace.project.read(cx);
5311                if predicate(project, cx) {
5312                    cx.activate_window();
5313                    true
5314                } else {
5315                    false
5316                }
5317            })
5318            .log_err()
5319            .unwrap_or(false);
5320
5321        if predicate {
5322            return Some(workspace);
5323        }
5324    }
5325
5326    None
5327}
5328
5329pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5330    DB.last_workspace().await.log_err().flatten()
5331}
5332
5333pub fn last_session_workspace_locations(
5334    last_session_id: &str,
5335    last_session_window_stack: Option<Vec<WindowId>>,
5336) -> Option<Vec<SerializedWorkspaceLocation>> {
5337    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5338        .log_err()
5339}
5340
5341actions!(collab, [OpenChannelNotes]);
5342actions!(zed, [OpenLog]);
5343
5344async fn join_channel_internal(
5345    channel_id: ChannelId,
5346    app_state: &Arc<AppState>,
5347    requesting_window: Option<WindowHandle<Workspace>>,
5348    active_call: &Model<ActiveCall>,
5349    cx: &mut AsyncAppContext,
5350) -> Result<bool> {
5351    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5352        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5353            return (false, None);
5354        };
5355
5356        let already_in_channel = room.channel_id() == Some(channel_id);
5357        let should_prompt = room.is_sharing_project()
5358            && !room.remote_participants().is_empty()
5359            && !already_in_channel;
5360        let open_room = if already_in_channel {
5361            active_call.room().cloned()
5362        } else {
5363            None
5364        };
5365        (should_prompt, open_room)
5366    })?;
5367
5368    if let Some(room) = open_room {
5369        let task = room.update(cx, |room, cx| {
5370            if let Some((project, host)) = room.most_active_project(cx) {
5371                return Some(join_in_room_project(project, host, app_state.clone(), cx));
5372            }
5373
5374            None
5375        })?;
5376        if let Some(task) = task {
5377            task.await?;
5378        }
5379        return anyhow::Ok(true);
5380    }
5381
5382    if should_prompt {
5383        if let Some(workspace) = requesting_window {
5384            let answer = workspace
5385                .update(cx, |_, cx| {
5386                    cx.prompt(
5387                        PromptLevel::Warning,
5388                        "Do you want to switch channels?",
5389                        Some("Leaving this call will unshare your current project."),
5390                        &["Yes, Join Channel", "Cancel"],
5391                    )
5392                })?
5393                .await;
5394
5395            if answer == Ok(1) {
5396                return Ok(false);
5397            }
5398        } else {
5399            return Ok(false); // unreachable!() hopefully
5400        }
5401    }
5402
5403    let client = cx.update(|cx| active_call.read(cx).client())?;
5404
5405    let mut client_status = client.status();
5406
5407    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5408    'outer: loop {
5409        let Some(status) = client_status.recv().await else {
5410            return Err(anyhow!("error connecting"));
5411        };
5412
5413        match status {
5414            Status::Connecting
5415            | Status::Authenticating
5416            | Status::Reconnecting
5417            | Status::Reauthenticating => continue,
5418            Status::Connected { .. } => break 'outer,
5419            Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5420            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5421            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5422                return Err(ErrorCode::Disconnected.into());
5423            }
5424        }
5425    }
5426
5427    let room = active_call
5428        .update(cx, |active_call, cx| {
5429            active_call.join_channel(channel_id, cx)
5430        })?
5431        .await?;
5432
5433    let Some(room) = room else {
5434        return anyhow::Ok(true);
5435    };
5436
5437    room.update(cx, |room, _| room.room_update_completed())?
5438        .await;
5439
5440    let task = room.update(cx, |room, cx| {
5441        if let Some((project, host)) = room.most_active_project(cx) {
5442            return Some(join_in_room_project(project, host, app_state.clone(), cx));
5443        }
5444
5445        // If you are the first to join a channel, see if you should share your project.
5446        if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5447            if let Some(workspace) = requesting_window {
5448                let project = workspace.update(cx, |workspace, cx| {
5449                    let project = workspace.project.read(cx);
5450
5451                    if !CallSettings::get_global(cx).share_on_join {
5452                        return None;
5453                    }
5454
5455                    if (project.is_local() || project.is_via_ssh())
5456                        && project.visible_worktrees(cx).any(|tree| {
5457                            tree.read(cx)
5458                                .root_entry()
5459                                .map_or(false, |entry| entry.is_dir())
5460                        })
5461                    {
5462                        Some(workspace.project.clone())
5463                    } else {
5464                        None
5465                    }
5466                });
5467                if let Ok(Some(project)) = project {
5468                    return Some(cx.spawn(|room, mut cx| async move {
5469                        room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5470                            .await?;
5471                        Ok(())
5472                    }));
5473                }
5474            }
5475        }
5476
5477        None
5478    })?;
5479    if let Some(task) = task {
5480        task.await?;
5481        return anyhow::Ok(true);
5482    }
5483    anyhow::Ok(false)
5484}
5485
5486pub fn join_channel(
5487    channel_id: ChannelId,
5488    app_state: Arc<AppState>,
5489    requesting_window: Option<WindowHandle<Workspace>>,
5490    cx: &mut AppContext,
5491) -> Task<Result<()>> {
5492    let active_call = ActiveCall::global(cx);
5493    cx.spawn(|mut cx| async move {
5494        let result = join_channel_internal(
5495            channel_id,
5496            &app_state,
5497            requesting_window,
5498            &active_call,
5499            &mut cx,
5500        )
5501            .await;
5502
5503        // join channel succeeded, and opened a window
5504        if matches!(result, Ok(true)) {
5505            return anyhow::Ok(());
5506        }
5507
5508        // find an existing workspace to focus and show call controls
5509        let mut active_window =
5510            requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5511        if active_window.is_none() {
5512            // no open workspaces, make one to show the error in (blergh)
5513            let (window_handle, _) = cx
5514                .update(|cx| {
5515                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5516                })?
5517                .await?;
5518
5519            if result.is_ok() {
5520                cx.update(|cx| {
5521                    cx.dispatch_action(&OpenChannelNotes);
5522                }).log_err();
5523            }
5524
5525            active_window = Some(window_handle);
5526        }
5527
5528        if let Err(err) = result {
5529            log::error!("failed to join channel: {}", err);
5530            if let Some(active_window) = active_window {
5531                active_window
5532                    .update(&mut cx, |_, cx| {
5533                        let detail: SharedString = match err.error_code() {
5534                            ErrorCode::SignedOut => {
5535                                "Please sign in to continue.".into()
5536                            }
5537                            ErrorCode::UpgradeRequired => {
5538                                "Your are running an unsupported version of Zed. Please update to continue.".into()
5539                            }
5540                            ErrorCode::NoSuchChannel => {
5541                                "No matching channel was found. Please check the link and try again.".into()
5542                            }
5543                            ErrorCode::Forbidden => {
5544                                "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5545                            }
5546                            ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5547                            _ => format!("{}\n\nPlease try again.", err).into(),
5548                        };
5549                        cx.prompt(
5550                            PromptLevel::Critical,
5551                            "Failed to join channel",
5552                            Some(&detail),
5553                            &["Ok"],
5554                        )
5555                    })?
5556                    .await
5557                    .ok();
5558            }
5559        }
5560
5561        // return ok, we showed the error to the user.
5562        anyhow::Ok(())
5563    })
5564}
5565
5566pub async fn get_any_active_workspace(
5567    app_state: Arc<AppState>,
5568    mut cx: AsyncAppContext,
5569) -> anyhow::Result<WindowHandle<Workspace>> {
5570    // find an existing workspace to focus and show call controls
5571    let active_window = activate_any_workspace_window(&mut cx);
5572    if active_window.is_none() {
5573        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5574            .await?;
5575    }
5576    activate_any_workspace_window(&mut cx).context("could not open zed")
5577}
5578
5579fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5580    cx.update(|cx| {
5581        if let Some(workspace_window) = cx
5582            .active_window()
5583            .and_then(|window| window.downcast::<Workspace>())
5584        {
5585            return Some(workspace_window);
5586        }
5587
5588        for window in cx.windows() {
5589            if let Some(workspace_window) = window.downcast::<Workspace>() {
5590                workspace_window
5591                    .update(cx, |_, cx| cx.activate_window())
5592                    .ok();
5593                return Some(workspace_window);
5594            }
5595        }
5596        None
5597    })
5598    .ok()
5599    .flatten()
5600}
5601
5602pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5603    cx.windows()
5604        .into_iter()
5605        .filter_map(|window| window.downcast::<Workspace>())
5606        .filter(|workspace| {
5607            workspace
5608                .read(cx)
5609                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5610        })
5611        .collect()
5612}
5613
5614#[derive(Default)]
5615pub struct OpenOptions {
5616    pub open_new_workspace: Option<bool>,
5617    pub replace_window: Option<WindowHandle<Workspace>>,
5618    pub env: Option<HashMap<String, String>>,
5619}
5620
5621#[allow(clippy::type_complexity)]
5622pub fn open_paths(
5623    abs_paths: &[PathBuf],
5624    app_state: Arc<AppState>,
5625    open_options: OpenOptions,
5626    cx: &mut AppContext,
5627) -> Task<
5628    anyhow::Result<(
5629        WindowHandle<Workspace>,
5630        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5631    )>,
5632> {
5633    let abs_paths = abs_paths.to_vec();
5634    let mut existing = None;
5635    let mut best_match = None;
5636    let mut open_visible = OpenVisible::All;
5637
5638    if open_options.open_new_workspace != Some(true) {
5639        for window in local_workspace_windows(cx) {
5640            if let Ok(workspace) = window.read(cx) {
5641                let m = workspace
5642                    .project
5643                    .read(cx)
5644                    .visibility_for_paths(&abs_paths, cx);
5645                if m > best_match {
5646                    existing = Some(window);
5647                    best_match = m;
5648                } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5649                    existing = Some(window)
5650                }
5651            }
5652        }
5653    }
5654
5655    cx.spawn(move |mut cx| async move {
5656        if open_options.open_new_workspace.is_none() && existing.is_none() {
5657            let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5658            if futures::future::join_all(all_files)
5659                .await
5660                .into_iter()
5661                .filter_map(|result| result.ok().flatten())
5662                .all(|file| !file.is_dir)
5663            {
5664                cx.update(|cx| {
5665                    for window in local_workspace_windows(cx) {
5666                        if let Ok(workspace) = window.read(cx) {
5667                            let project = workspace.project().read(cx);
5668                            if project.is_via_collab() {
5669                                continue;
5670                            }
5671                            existing = Some(window);
5672                            open_visible = OpenVisible::None;
5673                            break;
5674                        }
5675                    }
5676                })?;
5677            }
5678        }
5679
5680        if let Some(existing) = existing {
5681            let open_task = existing
5682                .update(&mut cx, |workspace, cx| {
5683                    cx.activate_window();
5684                    workspace.open_paths(abs_paths, open_visible, None, cx)
5685                })?
5686                .await;
5687
5688            _ = existing.update(&mut cx, |workspace, cx| {
5689                for item in open_task.iter().flatten() {
5690                    if let Err(e) = item {
5691                        workspace.show_error(&e, cx);
5692                    }
5693                }
5694            });
5695
5696            Ok((existing, open_task))
5697        } else {
5698            cx.update(move |cx| {
5699                Workspace::new_local(
5700                    abs_paths,
5701                    app_state.clone(),
5702                    open_options.replace_window,
5703                    open_options.env,
5704                    cx,
5705                )
5706            })?
5707            .await
5708        }
5709    })
5710}
5711
5712pub fn open_new(
5713    open_options: OpenOptions,
5714    app_state: Arc<AppState>,
5715    cx: &mut AppContext,
5716    init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5717) -> Task<anyhow::Result<()>> {
5718    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
5719    cx.spawn(|mut cx| async move {
5720        let (workspace, opened_paths) = task.await?;
5721        workspace.update(&mut cx, |workspace, cx| {
5722            if opened_paths.is_empty() {
5723                init(workspace, cx)
5724            }
5725        })?;
5726        Ok(())
5727    })
5728}
5729
5730pub fn create_and_open_local_file(
5731    path: &'static Path,
5732    cx: &mut ViewContext<Workspace>,
5733    default_content: impl 'static + Send + FnOnce() -> Rope,
5734) -> Task<Result<Box<dyn ItemHandle>>> {
5735    cx.spawn(|workspace, mut cx| async move {
5736        let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5737        if !fs.is_file(path).await {
5738            fs.create_file(path, Default::default()).await?;
5739            fs.save(path, &default_content(), Default::default())
5740                .await?;
5741        }
5742
5743        let mut items = workspace
5744            .update(&mut cx, |workspace, cx| {
5745                workspace.with_local_workspace(cx, |workspace, cx| {
5746                    workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5747                })
5748            })?
5749            .await?
5750            .await;
5751
5752        let item = items.pop().flatten();
5753        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5754    })
5755}
5756
5757pub fn open_ssh_project(
5758    window: WindowHandle<Workspace>,
5759    connection_options: SshConnectionOptions,
5760    cancel_rx: oneshot::Receiver<()>,
5761    delegate: Arc<dyn SshClientDelegate>,
5762    app_state: Arc<AppState>,
5763    paths: Vec<PathBuf>,
5764    cx: &mut AppContext,
5765) -> Task<Result<()>> {
5766    cx.spawn(|mut cx| async move {
5767        let (serialized_ssh_project, workspace_id, serialized_workspace) =
5768            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
5769
5770        let session = match cx
5771            .update(|cx| {
5772                remote::SshRemoteClient::new(
5773                    ConnectionIdentifier::Workspace(workspace_id.0),
5774                    connection_options,
5775                    cancel_rx,
5776                    delegate,
5777                    cx,
5778                )
5779            })?
5780            .await?
5781        {
5782            Some(result) => result,
5783            None => return Ok(()),
5784        };
5785
5786        let project = cx.update(|cx| {
5787            project::Project::ssh(
5788                session,
5789                app_state.client.clone(),
5790                app_state.node_runtime.clone(),
5791                app_state.user_store.clone(),
5792                app_state.languages.clone(),
5793                app_state.fs.clone(),
5794                cx,
5795            )
5796        })?;
5797
5798        let toolchains = DB.toolchains(workspace_id).await?;
5799        for (toolchain, worktree_id) in toolchains {
5800            project
5801                .update(&mut cx, |this, cx| {
5802                    this.activate_toolchain(worktree_id, toolchain, cx)
5803                })?
5804                .await;
5805        }
5806        let mut project_paths_to_open = vec![];
5807        let mut project_path_errors = vec![];
5808
5809        for path in paths {
5810            let result = cx
5811                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
5812                .await;
5813            match result {
5814                Ok((_, project_path)) => {
5815                    project_paths_to_open.push((path.clone(), Some(project_path)));
5816                }
5817                Err(error) => {
5818                    project_path_errors.push(error);
5819                }
5820            };
5821        }
5822
5823        if project_paths_to_open.is_empty() {
5824            return Err(project_path_errors
5825                .pop()
5826                .unwrap_or_else(|| anyhow!("no paths given")));
5827        }
5828
5829        cx.update_window(window.into(), |_, cx| {
5830            cx.replace_root_view(|cx| {
5831                let mut workspace =
5832                    Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
5833
5834                workspace
5835                    .client()
5836                    .telemetry()
5837                    .report_app_event("open ssh project".to_string());
5838
5839                workspace.set_serialized_ssh_project(serialized_ssh_project);
5840                workspace
5841            });
5842        })?;
5843
5844        window
5845            .update(&mut cx, |_, cx| {
5846                cx.activate_window();
5847
5848                open_items(serialized_workspace, project_paths_to_open, cx)
5849            })?
5850            .await?;
5851
5852        window.update(&mut cx, |workspace, cx| {
5853            for error in project_path_errors {
5854                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
5855                    if let Some(path) = error.error_tag("path") {
5856                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
5857                    }
5858                } else {
5859                    workspace.show_error(&error, cx)
5860                }
5861            }
5862        })
5863    })
5864}
5865
5866fn serialize_ssh_project(
5867    connection_options: SshConnectionOptions,
5868    paths: Vec<PathBuf>,
5869    cx: &AsyncAppContext,
5870) -> Task<
5871    Result<(
5872        SerializedSshProject,
5873        WorkspaceId,
5874        Option<SerializedWorkspace>,
5875    )>,
5876> {
5877    cx.background_executor().spawn(async move {
5878        let serialized_ssh_project = persistence::DB
5879            .get_or_create_ssh_project(
5880                connection_options.host.clone(),
5881                connection_options.port,
5882                paths
5883                    .iter()
5884                    .map(|path| path.to_string_lossy().to_string())
5885                    .collect::<Vec<_>>(),
5886                connection_options.username.clone(),
5887            )
5888            .await?;
5889
5890        let serialized_workspace =
5891            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
5892
5893        let workspace_id = if let Some(workspace_id) =
5894            serialized_workspace.as_ref().map(|workspace| workspace.id)
5895        {
5896            workspace_id
5897        } else {
5898            persistence::DB.next_id().await?
5899        };
5900
5901        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
5902    })
5903}
5904
5905pub fn join_in_room_project(
5906    project_id: u64,
5907    follow_user_id: u64,
5908    app_state: Arc<AppState>,
5909    cx: &mut AppContext,
5910) -> Task<Result<()>> {
5911    let windows = cx.windows();
5912    cx.spawn(|mut cx| async move {
5913        let existing_workspace = windows.into_iter().find_map(|window| {
5914            window.downcast::<Workspace>().and_then(|window| {
5915                window
5916                    .update(&mut cx, |workspace, cx| {
5917                        if workspace.project().read(cx).remote_id() == Some(project_id) {
5918                            Some(window)
5919                        } else {
5920                            None
5921                        }
5922                    })
5923                    .unwrap_or(None)
5924            })
5925        });
5926
5927        let workspace = if let Some(existing_workspace) = existing_workspace {
5928            existing_workspace
5929        } else {
5930            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5931            let room = active_call
5932                .read_with(&cx, |call, _| call.room().cloned())?
5933                .ok_or_else(|| anyhow!("not in a call"))?;
5934            let project = room
5935                .update(&mut cx, |room, cx| {
5936                    room.join_project(
5937                        project_id,
5938                        app_state.languages.clone(),
5939                        app_state.fs.clone(),
5940                        cx,
5941                    )
5942                })?
5943                .await?;
5944
5945            let window_bounds_override = window_bounds_env_override();
5946            cx.update(|cx| {
5947                let mut options = (app_state.build_window_options)(None, cx);
5948                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5949                cx.open_window(options, |cx| {
5950                    cx.new_view(|cx| {
5951                        Workspace::new(Default::default(), project, app_state.clone(), cx)
5952                    })
5953                })
5954            })??
5955        };
5956
5957        workspace.update(&mut cx, |workspace, cx| {
5958            cx.activate(true);
5959            cx.activate_window();
5960
5961            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5962                let follow_peer_id = room
5963                    .read(cx)
5964                    .remote_participants()
5965                    .iter()
5966                    .find(|(_, participant)| participant.user.id == follow_user_id)
5967                    .map(|(_, p)| p.peer_id)
5968                    .or_else(|| {
5969                        // If we couldn't follow the given user, follow the host instead.
5970                        let collaborator = workspace
5971                            .project()
5972                            .read(cx)
5973                            .collaborators()
5974                            .values()
5975                            .find(|collaborator| collaborator.is_host)?;
5976                        Some(collaborator.peer_id)
5977                    });
5978
5979                if let Some(follow_peer_id) = follow_peer_id {
5980                    workspace.follow(follow_peer_id, cx);
5981                }
5982            }
5983        })?;
5984
5985        anyhow::Ok(())
5986    })
5987}
5988
5989pub fn reload(reload: &Reload, cx: &mut AppContext) {
5990    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5991    let mut workspace_windows = cx
5992        .windows()
5993        .into_iter()
5994        .filter_map(|window| window.downcast::<Workspace>())
5995        .collect::<Vec<_>>();
5996
5997    // If multiple windows have unsaved changes, and need a save prompt,
5998    // prompt in the active window before switching to a different window.
5999    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6000
6001    let mut prompt = None;
6002    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6003        prompt = window
6004            .update(cx, |_, cx| {
6005                cx.prompt(
6006                    PromptLevel::Info,
6007                    "Are you sure you want to restart?",
6008                    None,
6009                    &["Restart", "Cancel"],
6010                )
6011            })
6012            .ok();
6013    }
6014
6015    let binary_path = reload.binary_path.clone();
6016    cx.spawn(|mut cx| async move {
6017        if let Some(prompt) = prompt {
6018            let answer = prompt.await?;
6019            if answer != 0 {
6020                return Ok(());
6021            }
6022        }
6023
6024        // If the user cancels any save prompt, then keep the app open.
6025        for window in workspace_windows {
6026            if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
6027                workspace.prepare_to_close(CloseIntent::Quit, cx)
6028            }) {
6029                if !should_close.await? {
6030                    return Ok(());
6031                }
6032            }
6033        }
6034
6035        cx.update(|cx| cx.restart(binary_path))
6036    })
6037    .detach_and_log_err(cx);
6038}
6039
6040fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6041    let mut parts = value.split(',');
6042    let x: usize = parts.next()?.parse().ok()?;
6043    let y: usize = parts.next()?.parse().ok()?;
6044    Some(point(px(x as f32), px(y as f32)))
6045}
6046
6047fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6048    let mut parts = value.split(',');
6049    let width: usize = parts.next()?.parse().ok()?;
6050    let height: usize = parts.next()?.parse().ok()?;
6051    Some(size(px(width as f32), px(height as f32)))
6052}
6053
6054pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
6055    const BORDER_SIZE: Pixels = px(1.0);
6056    let decorations = cx.window_decorations();
6057
6058    if matches!(decorations, Decorations::Client { .. }) {
6059        cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6060    }
6061
6062    struct GlobalResizeEdge(ResizeEdge);
6063    impl Global for GlobalResizeEdge {}
6064
6065    div()
6066        .id("window-backdrop")
6067        .bg(transparent_black())
6068        .map(|div| match decorations {
6069            Decorations::Server => div,
6070            Decorations::Client { tiling, .. } => div
6071                .when(!(tiling.top || tiling.right), |div| {
6072                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6073                })
6074                .when(!(tiling.top || tiling.left), |div| {
6075                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6076                })
6077                .when(!(tiling.bottom || tiling.right), |div| {
6078                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6079                })
6080                .when(!(tiling.bottom || tiling.left), |div| {
6081                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6082                })
6083                .when(!tiling.top, |div| {
6084                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6085                })
6086                .when(!tiling.bottom, |div| {
6087                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6088                })
6089                .when(!tiling.left, |div| {
6090                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6091                })
6092                .when(!tiling.right, |div| {
6093                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6094                })
6095                .on_mouse_move(move |e, cx| {
6096                    let size = cx.window_bounds().get_bounds().size;
6097                    let pos = e.position;
6098
6099                    let new_edge =
6100                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6101
6102                    let edge = cx.try_global::<GlobalResizeEdge>();
6103                    if new_edge != edge.map(|edge| edge.0) {
6104                        cx.window_handle()
6105                            .update(cx, |workspace, cx| cx.notify(Some(workspace.entity_id())))
6106                            .ok();
6107                    }
6108                })
6109                .on_mouse_down(MouseButton::Left, move |e, cx| {
6110                    let size = cx.window_bounds().get_bounds().size;
6111                    let pos = e.position;
6112
6113                    let edge = match resize_edge(
6114                        pos,
6115                        theme::CLIENT_SIDE_DECORATION_SHADOW,
6116                        size,
6117                        tiling,
6118                    ) {
6119                        Some(value) => value,
6120                        None => return,
6121                    };
6122
6123                    cx.start_window_resize(edge);
6124                }),
6125        })
6126        .size_full()
6127        .child(
6128            div()
6129                .cursor(CursorStyle::Arrow)
6130                .map(|div| match decorations {
6131                    Decorations::Server => div,
6132                    Decorations::Client { tiling } => div
6133                        .border_color(cx.theme().colors().border)
6134                        .when(!(tiling.top || tiling.right), |div| {
6135                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6136                        })
6137                        .when(!(tiling.top || tiling.left), |div| {
6138                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6139                        })
6140                        .when(!(tiling.bottom || tiling.right), |div| {
6141                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6142                        })
6143                        .when(!(tiling.bottom || tiling.left), |div| {
6144                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6145                        })
6146                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6147                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6148                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6149                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6150                        .when(!tiling.is_tiled(), |div| {
6151                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
6152                                color: Hsla {
6153                                    h: 0.,
6154                                    s: 0.,
6155                                    l: 0.,
6156                                    a: 0.4,
6157                                },
6158                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6159                                spread_radius: px(0.),
6160                                offset: point(px(0.0), px(0.0)),
6161                            }])
6162                        }),
6163                })
6164                .on_mouse_move(|_e, cx| {
6165                    cx.stop_propagation();
6166                })
6167                .size_full()
6168                .child(element),
6169        )
6170        .map(|div| match decorations {
6171            Decorations::Server => div,
6172            Decorations::Client { tiling, .. } => div.child(
6173                canvas(
6174                    |_bounds, cx| {
6175                        cx.insert_hitbox(
6176                            Bounds::new(
6177                                point(px(0.0), px(0.0)),
6178                                cx.window_bounds().get_bounds().size,
6179                            ),
6180                            false,
6181                        )
6182                    },
6183                    move |_bounds, hitbox, cx| {
6184                        let mouse = cx.mouse_position();
6185                        let size = cx.window_bounds().get_bounds().size;
6186                        let Some(edge) =
6187                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6188                        else {
6189                            return;
6190                        };
6191                        cx.set_global(GlobalResizeEdge(edge));
6192                        cx.set_cursor_style(
6193                            match edge {
6194                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6195                                ResizeEdge::Left | ResizeEdge::Right => {
6196                                    CursorStyle::ResizeLeftRight
6197                                }
6198                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6199                                    CursorStyle::ResizeUpLeftDownRight
6200                                }
6201                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6202                                    CursorStyle::ResizeUpRightDownLeft
6203                                }
6204                            },
6205                            &hitbox,
6206                        );
6207                    },
6208                )
6209                .size_full()
6210                .absolute(),
6211            ),
6212        })
6213}
6214
6215fn resize_edge(
6216    pos: Point<Pixels>,
6217    shadow_size: Pixels,
6218    window_size: Size<Pixels>,
6219    tiling: Tiling,
6220) -> Option<ResizeEdge> {
6221    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6222    if bounds.contains(&pos) {
6223        return None;
6224    }
6225
6226    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6227    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6228    if !tiling.top && top_left_bounds.contains(&pos) {
6229        return Some(ResizeEdge::TopLeft);
6230    }
6231
6232    let top_right_bounds = Bounds::new(
6233        Point::new(window_size.width - corner_size.width, px(0.)),
6234        corner_size,
6235    );
6236    if !tiling.top && top_right_bounds.contains(&pos) {
6237        return Some(ResizeEdge::TopRight);
6238    }
6239
6240    let bottom_left_bounds = Bounds::new(
6241        Point::new(px(0.), window_size.height - corner_size.height),
6242        corner_size,
6243    );
6244    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6245        return Some(ResizeEdge::BottomLeft);
6246    }
6247
6248    let bottom_right_bounds = Bounds::new(
6249        Point::new(
6250            window_size.width - corner_size.width,
6251            window_size.height - corner_size.height,
6252        ),
6253        corner_size,
6254    );
6255    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6256        return Some(ResizeEdge::BottomRight);
6257    }
6258
6259    if !tiling.top && pos.y < shadow_size {
6260        Some(ResizeEdge::Top)
6261    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6262        Some(ResizeEdge::Bottom)
6263    } else if !tiling.left && pos.x < shadow_size {
6264        Some(ResizeEdge::Left)
6265    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6266        Some(ResizeEdge::Right)
6267    } else {
6268        None
6269    }
6270}
6271
6272fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext) {
6273    if pane == active_pane {
6274        return;
6275    } else if pane.read(cx).items_len() == 0 {
6276        pane.update(cx, |_, cx| {
6277            cx.emit(pane::Event::Remove {
6278                focus_on_pane: None,
6279            });
6280        })
6281    } else {
6282        move_all_items(pane, active_pane, cx);
6283    }
6284}
6285
6286fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext) {
6287    let destination_is_different = from_pane != to_pane;
6288    let mut moved_items = 0;
6289    for (item_ix, item_handle) in from_pane
6290        .read(cx)
6291        .items()
6292        .enumerate()
6293        .map(|(ix, item)| (ix, item.clone()))
6294        .collect::<Vec<_>>()
6295    {
6296        let ix = item_ix - moved_items;
6297        if destination_is_different {
6298            // Close item from previous pane
6299            from_pane.update(cx, |source, cx| {
6300                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
6301            });
6302            moved_items += 1;
6303        }
6304
6305        // This automatically removes duplicate items in the pane
6306        to_pane.update(cx, |destination, cx| {
6307            destination.add_item(item_handle, true, true, None, cx);
6308            destination.focus(cx)
6309        });
6310    }
6311}
6312
6313pub fn move_item(
6314    source: &View<Pane>,
6315    destination: &View<Pane>,
6316    item_id_to_move: EntityId,
6317    destination_index: usize,
6318    cx: &mut WindowContext,
6319) {
6320    let Some((item_ix, item_handle)) = source
6321        .read(cx)
6322        .items()
6323        .enumerate()
6324        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6325        .map(|(ix, item)| (ix, item.clone()))
6326    else {
6327        // Tab was closed during drag
6328        return;
6329    };
6330
6331    if source != destination {
6332        // Close item from previous pane
6333        source.update(cx, |source, cx| {
6334            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
6335        });
6336    }
6337
6338    // This automatically removes duplicate items in the pane
6339    destination.update(cx, |destination, cx| {
6340        destination.add_item(item_handle, true, true, Some(destination_index), cx);
6341        destination.focus(cx)
6342    });
6343}
6344
6345pub fn move_active_item(
6346    source: &View<Pane>,
6347    destination: &View<Pane>,
6348    focus_destination: bool,
6349    close_if_empty: bool,
6350    cx: &mut WindowContext,
6351) {
6352    if source == destination {
6353        return;
6354    }
6355    let Some(active_item) = source.read(cx).active_item() else {
6356        return;
6357    };
6358    source.update(cx, |source_pane, cx| {
6359        let item_id = active_item.item_id();
6360        source_pane.remove_item(item_id, false, close_if_empty, cx);
6361        destination.update(cx, |target_pane, cx| {
6362            target_pane.add_item(
6363                active_item,
6364                focus_destination,
6365                focus_destination,
6366                Some(target_pane.items_len()),
6367                cx,
6368            );
6369        });
6370    });
6371}
6372
6373#[cfg(test)]
6374mod tests {
6375    use std::{cell::RefCell, rc::Rc};
6376
6377    use super::*;
6378    use crate::{
6379        dock::{test::TestPanel, PanelEvent},
6380        item::{
6381            test::{TestItem, TestProjectItem},
6382            ItemEvent,
6383        },
6384    };
6385    use fs::FakeFs;
6386    use gpui::{
6387        px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6388        UpdateGlobal, VisualTestContext,
6389    };
6390    use project::{Project, ProjectEntryId};
6391    use serde_json::json;
6392    use settings::SettingsStore;
6393
6394    #[gpui::test]
6395    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6396        init_test(cx);
6397
6398        let fs = FakeFs::new(cx.executor());
6399        let project = Project::test(fs, [], cx).await;
6400        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6401
6402        // Adding an item with no ambiguity renders the tab without detail.
6403        let item1 = cx.new_view(|cx| {
6404            let mut item = TestItem::new(cx);
6405            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6406            item
6407        });
6408        workspace.update(cx, |workspace, cx| {
6409            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6410        });
6411        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6412
6413        // Adding an item that creates ambiguity increases the level of detail on
6414        // both tabs.
6415        let item2 = cx.new_view(|cx| {
6416            let mut item = TestItem::new(cx);
6417            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6418            item
6419        });
6420        workspace.update(cx, |workspace, cx| {
6421            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6422        });
6423        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6424        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6425
6426        // Adding an item that creates ambiguity increases the level of detail only
6427        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6428        // we stop at the highest detail available.
6429        let item3 = cx.new_view(|cx| {
6430            let mut item = TestItem::new(cx);
6431            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6432            item
6433        });
6434        workspace.update(cx, |workspace, cx| {
6435            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6436        });
6437        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6438        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6439        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6440    }
6441
6442    #[gpui::test]
6443    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6444        init_test(cx);
6445
6446        let fs = FakeFs::new(cx.executor());
6447        fs.insert_tree(
6448            "/root1",
6449            json!({
6450                "one.txt": "",
6451                "two.txt": "",
6452            }),
6453        )
6454        .await;
6455        fs.insert_tree(
6456            "/root2",
6457            json!({
6458                "three.txt": "",
6459            }),
6460        )
6461        .await;
6462
6463        let project = Project::test(fs, ["root1".as_ref()], cx).await;
6464        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6465        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6466        let worktree_id = project.update(cx, |project, cx| {
6467            project.worktrees(cx).next().unwrap().read(cx).id()
6468        });
6469
6470        let item1 = cx.new_view(|cx| {
6471            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6472        });
6473        let item2 = cx.new_view(|cx| {
6474            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6475        });
6476
6477        // Add an item to an empty pane
6478        workspace.update(cx, |workspace, cx| {
6479            workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6480        });
6481        project.update(cx, |project, cx| {
6482            assert_eq!(
6483                project.active_entry(),
6484                project
6485                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6486                    .map(|e| e.id)
6487            );
6488        });
6489        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6490
6491        // Add a second item to a non-empty pane
6492        workspace.update(cx, |workspace, cx| {
6493            workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6494        });
6495        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6496        project.update(cx, |project, cx| {
6497            assert_eq!(
6498                project.active_entry(),
6499                project
6500                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6501                    .map(|e| e.id)
6502            );
6503        });
6504
6505        // Close the active item
6506        pane.update(cx, |pane, cx| {
6507            pane.close_active_item(&Default::default(), cx).unwrap()
6508        })
6509        .await
6510        .unwrap();
6511        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6512        project.update(cx, |project, cx| {
6513            assert_eq!(
6514                project.active_entry(),
6515                project
6516                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6517                    .map(|e| e.id)
6518            );
6519        });
6520
6521        // Add a project folder
6522        project
6523            .update(cx, |project, cx| {
6524                project.find_or_create_worktree("root2", true, cx)
6525            })
6526            .await
6527            .unwrap();
6528        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6529
6530        // Remove a project folder
6531        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6532        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6533    }
6534
6535    #[gpui::test]
6536    async fn test_close_window(cx: &mut TestAppContext) {
6537        init_test(cx);
6538
6539        let fs = FakeFs::new(cx.executor());
6540        fs.insert_tree("/root", json!({ "one": "" })).await;
6541
6542        let project = Project::test(fs, ["root".as_ref()], cx).await;
6543        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6544
6545        // When there are no dirty items, there's nothing to do.
6546        let item1 = cx.new_view(TestItem::new);
6547        workspace.update(cx, |w, cx| {
6548            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6549        });
6550        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6551        assert!(task.await.unwrap());
6552
6553        // When there are dirty untitled items, prompt to save each one. If the user
6554        // cancels any prompt, then abort.
6555        let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6556        let item3 = cx.new_view(|cx| {
6557            TestItem::new(cx)
6558                .with_dirty(true)
6559                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6560        });
6561        workspace.update(cx, |w, cx| {
6562            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6563            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6564        });
6565        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6566        cx.executor().run_until_parked();
6567        cx.simulate_prompt_answer(2); // cancel save all
6568        cx.executor().run_until_parked();
6569        cx.simulate_prompt_answer(2); // cancel save all
6570        cx.executor().run_until_parked();
6571        assert!(!cx.has_pending_prompt());
6572        assert!(!task.await.unwrap());
6573    }
6574
6575    #[gpui::test]
6576    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6577        init_test(cx);
6578
6579        // Register TestItem as a serializable item
6580        cx.update(|cx| {
6581            register_serializable_item::<TestItem>(cx);
6582        });
6583
6584        let fs = FakeFs::new(cx.executor());
6585        fs.insert_tree("/root", json!({ "one": "" })).await;
6586
6587        let project = Project::test(fs, ["root".as_ref()], cx).await;
6588        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6589
6590        // When there are dirty untitled items, but they can serialize, then there is no prompt.
6591        let item1 = cx.new_view(|cx| {
6592            TestItem::new(cx)
6593                .with_dirty(true)
6594                .with_serialize(|| Some(Task::ready(Ok(()))))
6595        });
6596        let item2 = cx.new_view(|cx| {
6597            TestItem::new(cx)
6598                .with_dirty(true)
6599                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6600                .with_serialize(|| Some(Task::ready(Ok(()))))
6601        });
6602        workspace.update(cx, |w, cx| {
6603            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6604            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6605        });
6606        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6607        assert!(task.await.unwrap());
6608    }
6609
6610    #[gpui::test]
6611    async fn test_close_pane_items(cx: &mut TestAppContext) {
6612        init_test(cx);
6613
6614        let fs = FakeFs::new(cx.executor());
6615
6616        let project = Project::test(fs, None, cx).await;
6617        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6618
6619        let item1 = cx.new_view(|cx| {
6620            TestItem::new(cx)
6621                .with_dirty(true)
6622                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
6623        });
6624        let item2 = cx.new_view(|cx| {
6625            TestItem::new(cx)
6626                .with_dirty(true)
6627                .with_conflict(true)
6628                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
6629        });
6630        let item3 = cx.new_view(|cx| {
6631            TestItem::new(cx)
6632                .with_dirty(true)
6633                .with_conflict(true)
6634                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
6635        });
6636        let item4 = cx.new_view(|cx| {
6637            TestItem::new(cx).with_dirty(true).with_project_items(&[{
6638                let project_item = TestProjectItem::new_untitled(cx);
6639                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6640                project_item
6641            }])
6642        });
6643        let pane = workspace.update(cx, |workspace, cx| {
6644            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6645            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6646            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6647            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6648            workspace.active_pane().clone()
6649        });
6650
6651        let close_items = pane.update(cx, |pane, cx| {
6652            pane.activate_item(1, true, true, cx);
6653            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6654            let item1_id = item1.item_id();
6655            let item3_id = item3.item_id();
6656            let item4_id = item4.item_id();
6657            pane.close_items(cx, SaveIntent::Close, move |id| {
6658                [item1_id, item3_id, item4_id].contains(&id)
6659            })
6660        });
6661        cx.executor().run_until_parked();
6662
6663        assert!(cx.has_pending_prompt());
6664        // Ignore "Save all" prompt
6665        cx.simulate_prompt_answer(2);
6666        cx.executor().run_until_parked();
6667        // There's a prompt to save item 1.
6668        pane.update(cx, |pane, _| {
6669            assert_eq!(pane.items_len(), 4);
6670            assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6671        });
6672        // Confirm saving item 1.
6673        cx.simulate_prompt_answer(0);
6674        cx.executor().run_until_parked();
6675
6676        // Item 1 is saved. There's a prompt to save item 3.
6677        pane.update(cx, |pane, cx| {
6678            assert_eq!(item1.read(cx).save_count, 1);
6679            assert_eq!(item1.read(cx).save_as_count, 0);
6680            assert_eq!(item1.read(cx).reload_count, 0);
6681            assert_eq!(pane.items_len(), 3);
6682            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6683        });
6684        assert!(cx.has_pending_prompt());
6685
6686        // Cancel saving item 3.
6687        cx.simulate_prompt_answer(1);
6688        cx.executor().run_until_parked();
6689
6690        // Item 3 is reloaded. There's a prompt to save item 4.
6691        pane.update(cx, |pane, cx| {
6692            assert_eq!(item3.read(cx).save_count, 0);
6693            assert_eq!(item3.read(cx).save_as_count, 0);
6694            assert_eq!(item3.read(cx).reload_count, 1);
6695            assert_eq!(pane.items_len(), 2);
6696            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6697        });
6698        assert!(cx.has_pending_prompt());
6699
6700        // Confirm saving item 4.
6701        cx.simulate_prompt_answer(0);
6702        cx.executor().run_until_parked();
6703
6704        // There's a prompt for a path for item 4.
6705        cx.simulate_new_path_selection(|_| Some(Default::default()));
6706        close_items.await.unwrap();
6707
6708        // The requested items are closed.
6709        pane.update(cx, |pane, cx| {
6710            assert_eq!(item4.read(cx).save_count, 0);
6711            assert_eq!(item4.read(cx).save_as_count, 1);
6712            assert_eq!(item4.read(cx).reload_count, 0);
6713            assert_eq!(pane.items_len(), 1);
6714            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6715        });
6716    }
6717
6718    #[gpui::test]
6719    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6720        init_test(cx);
6721
6722        let fs = FakeFs::new(cx.executor());
6723        let project = Project::test(fs, [], cx).await;
6724        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6725
6726        // Create several workspace items with single project entries, and two
6727        // workspace items with multiple project entries.
6728        let single_entry_items = (0..=4)
6729            .map(|project_entry_id| {
6730                cx.new_view(|cx| {
6731                    TestItem::new(cx)
6732                        .with_dirty(true)
6733                        .with_project_items(&[dirty_project_item(
6734                            project_entry_id,
6735                            &format!("{project_entry_id}.txt"),
6736                            cx,
6737                        )])
6738                })
6739            })
6740            .collect::<Vec<_>>();
6741        let item_2_3 = cx.new_view(|cx| {
6742            TestItem::new(cx)
6743                .with_dirty(true)
6744                .with_singleton(false)
6745                .with_project_items(&[
6746                    single_entry_items[2].read(cx).project_items[0].clone(),
6747                    single_entry_items[3].read(cx).project_items[0].clone(),
6748                ])
6749        });
6750        let item_3_4 = cx.new_view(|cx| {
6751            TestItem::new(cx)
6752                .with_dirty(true)
6753                .with_singleton(false)
6754                .with_project_items(&[
6755                    single_entry_items[3].read(cx).project_items[0].clone(),
6756                    single_entry_items[4].read(cx).project_items[0].clone(),
6757                ])
6758        });
6759
6760        // Create two panes that contain the following project entries:
6761        //   left pane:
6762        //     multi-entry items:   (2, 3)
6763        //     single-entry items:  0, 1, 2, 3, 4
6764        //   right pane:
6765        //     single-entry items:  1
6766        //     multi-entry items:   (3, 4)
6767        let left_pane = workspace.update(cx, |workspace, cx| {
6768            let left_pane = workspace.active_pane().clone();
6769            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6770            for item in single_entry_items {
6771                workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6772            }
6773            left_pane.update(cx, |pane, cx| {
6774                pane.activate_item(2, true, true, cx);
6775            });
6776
6777            let right_pane = workspace
6778                .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6779                .unwrap();
6780
6781            right_pane.update(cx, |pane, cx| {
6782                pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6783            });
6784
6785            left_pane
6786        });
6787
6788        cx.focus_view(&left_pane);
6789
6790        // When closing all of the items in the left pane, we should be prompted twice:
6791        // once for project entry 0, and once for project entry 2. Project entries 1,
6792        // 3, and 4 are all still open in the other paten. After those two
6793        // prompts, the task should complete.
6794
6795        let close = left_pane.update(cx, |pane, cx| {
6796            pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6797        });
6798        cx.executor().run_until_parked();
6799
6800        // Discard "Save all" prompt
6801        cx.simulate_prompt_answer(2);
6802
6803        cx.executor().run_until_parked();
6804        left_pane.update(cx, |pane, cx| {
6805            assert_eq!(
6806                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6807                &[ProjectEntryId::from_proto(0)]
6808            );
6809        });
6810        cx.simulate_prompt_answer(0);
6811
6812        cx.executor().run_until_parked();
6813        left_pane.update(cx, |pane, cx| {
6814            assert_eq!(
6815                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6816                &[ProjectEntryId::from_proto(2)]
6817            );
6818        });
6819        cx.simulate_prompt_answer(0);
6820
6821        cx.executor().run_until_parked();
6822        close.await.unwrap();
6823        left_pane.update(cx, |pane, _| {
6824            assert_eq!(pane.items_len(), 0);
6825        });
6826    }
6827
6828    #[gpui::test]
6829    async fn test_autosave(cx: &mut gpui::TestAppContext) {
6830        init_test(cx);
6831
6832        let fs = FakeFs::new(cx.executor());
6833        let project = Project::test(fs, [], cx).await;
6834        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6835        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6836
6837        let item = cx.new_view(|cx| {
6838            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6839        });
6840        let item_id = item.entity_id();
6841        workspace.update(cx, |workspace, cx| {
6842            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6843        });
6844
6845        // Autosave on window change.
6846        item.update(cx, |item, cx| {
6847            SettingsStore::update_global(cx, |settings, cx| {
6848                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6849                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
6850                })
6851            });
6852            item.is_dirty = true;
6853        });
6854
6855        // Deactivating the window saves the file.
6856        cx.deactivate_window();
6857        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6858
6859        // Re-activating the window doesn't save the file.
6860        cx.update(|cx| cx.activate_window());
6861        cx.executor().run_until_parked();
6862        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6863
6864        // Autosave on focus change.
6865        item.update(cx, |item, cx| {
6866            cx.focus_self();
6867            SettingsStore::update_global(cx, |settings, cx| {
6868                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6869                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
6870                })
6871            });
6872            item.is_dirty = true;
6873        });
6874
6875        // Blurring the item saves the file.
6876        item.update(cx, |_, cx| cx.blur());
6877        cx.executor().run_until_parked();
6878        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6879
6880        // Deactivating the window still saves the file.
6881        item.update(cx, |item, cx| {
6882            cx.focus_self();
6883            item.is_dirty = true;
6884        });
6885        cx.deactivate_window();
6886        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6887
6888        // Autosave after delay.
6889        item.update(cx, |item, cx| {
6890            SettingsStore::update_global(cx, |settings, cx| {
6891                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6892                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6893                })
6894            });
6895            item.is_dirty = true;
6896            cx.emit(ItemEvent::Edit);
6897        });
6898
6899        // Delay hasn't fully expired, so the file is still dirty and unsaved.
6900        cx.executor().advance_clock(Duration::from_millis(250));
6901        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6902
6903        // After delay expires, the file is saved.
6904        cx.executor().advance_clock(Duration::from_millis(250));
6905        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6906
6907        // Autosave on focus change, ensuring closing the tab counts as such.
6908        item.update(cx, |item, cx| {
6909            SettingsStore::update_global(cx, |settings, cx| {
6910                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6911                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
6912                })
6913            });
6914            item.is_dirty = true;
6915            for project_item in &mut item.project_items {
6916                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6917            }
6918        });
6919
6920        pane.update(cx, |pane, cx| {
6921            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6922        })
6923        .await
6924        .unwrap();
6925        assert!(!cx.has_pending_prompt());
6926        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6927
6928        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6929        workspace.update(cx, |workspace, cx| {
6930            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6931        });
6932        item.update(cx, |item, cx| {
6933            item.project_items[0].update(cx, |item, _| {
6934                item.entry_id = None;
6935            });
6936            item.is_dirty = true;
6937            cx.blur();
6938        });
6939        cx.run_until_parked();
6940        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6941
6942        // Ensure autosave is prevented for deleted files also when closing the buffer.
6943        let _close_items = pane.update(cx, |pane, cx| {
6944            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6945        });
6946        cx.run_until_parked();
6947        assert!(cx.has_pending_prompt());
6948        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6949    }
6950
6951    #[gpui::test]
6952    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6953        init_test(cx);
6954
6955        let fs = FakeFs::new(cx.executor());
6956
6957        let project = Project::test(fs, [], cx).await;
6958        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6959
6960        let item = cx.new_view(|cx| {
6961            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6962        });
6963        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6964        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6965        let toolbar_notify_count = Rc::new(RefCell::new(0));
6966
6967        workspace.update(cx, |workspace, cx| {
6968            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6969            let toolbar_notification_count = toolbar_notify_count.clone();
6970            cx.observe(&toolbar, move |_, _, _| {
6971                *toolbar_notification_count.borrow_mut() += 1
6972            })
6973            .detach();
6974        });
6975
6976        pane.update(cx, |pane, _| {
6977            assert!(!pane.can_navigate_backward());
6978            assert!(!pane.can_navigate_forward());
6979        });
6980
6981        item.update(cx, |item, cx| {
6982            item.set_state("one".to_string(), cx);
6983        });
6984
6985        // Toolbar must be notified to re-render the navigation buttons
6986        assert_eq!(*toolbar_notify_count.borrow(), 1);
6987
6988        pane.update(cx, |pane, _| {
6989            assert!(pane.can_navigate_backward());
6990            assert!(!pane.can_navigate_forward());
6991        });
6992
6993        workspace
6994            .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6995            .await
6996            .unwrap();
6997
6998        assert_eq!(*toolbar_notify_count.borrow(), 2);
6999        pane.update(cx, |pane, _| {
7000            assert!(!pane.can_navigate_backward());
7001            assert!(pane.can_navigate_forward());
7002        });
7003    }
7004
7005    #[gpui::test]
7006    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7007        init_test(cx);
7008        let fs = FakeFs::new(cx.executor());
7009
7010        let project = Project::test(fs, [], cx).await;
7011        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7012
7013        let panel = workspace.update(cx, |workspace, cx| {
7014            let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7015            workspace.add_panel(panel.clone(), cx);
7016
7017            workspace
7018                .right_dock()
7019                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
7020
7021            panel
7022        });
7023
7024        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7025        pane.update(cx, |pane, cx| {
7026            let item = cx.new_view(TestItem::new);
7027            pane.add_item(Box::new(item), true, true, None, cx);
7028        });
7029
7030        // Transfer focus from center to panel
7031        workspace.update(cx, |workspace, cx| {
7032            workspace.toggle_panel_focus::<TestPanel>(cx);
7033        });
7034
7035        workspace.update(cx, |workspace, cx| {
7036            assert!(workspace.right_dock().read(cx).is_open());
7037            assert!(!panel.is_zoomed(cx));
7038            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7039        });
7040
7041        // Transfer focus from panel to center
7042        workspace.update(cx, |workspace, cx| {
7043            workspace.toggle_panel_focus::<TestPanel>(cx);
7044        });
7045
7046        workspace.update(cx, |workspace, cx| {
7047            assert!(workspace.right_dock().read(cx).is_open());
7048            assert!(!panel.is_zoomed(cx));
7049            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
7050        });
7051
7052        // Close the dock
7053        workspace.update(cx, |workspace, cx| {
7054            workspace.toggle_dock(DockPosition::Right, cx);
7055        });
7056
7057        workspace.update(cx, |workspace, cx| {
7058            assert!(!workspace.right_dock().read(cx).is_open());
7059            assert!(!panel.is_zoomed(cx));
7060            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
7061        });
7062
7063        // Open the dock
7064        workspace.update(cx, |workspace, cx| {
7065            workspace.toggle_dock(DockPosition::Right, cx);
7066        });
7067
7068        workspace.update(cx, |workspace, cx| {
7069            assert!(workspace.right_dock().read(cx).is_open());
7070            assert!(!panel.is_zoomed(cx));
7071            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7072        });
7073
7074        // Focus and zoom panel
7075        panel.update(cx, |panel, cx| {
7076            cx.focus_self();
7077            panel.set_zoomed(true, cx)
7078        });
7079
7080        workspace.update(cx, |workspace, cx| {
7081            assert!(workspace.right_dock().read(cx).is_open());
7082            assert!(panel.is_zoomed(cx));
7083            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7084        });
7085
7086        // Transfer focus to the center closes the dock
7087        workspace.update(cx, |workspace, cx| {
7088            workspace.toggle_panel_focus::<TestPanel>(cx);
7089        });
7090
7091        workspace.update(cx, |workspace, cx| {
7092            assert!(!workspace.right_dock().read(cx).is_open());
7093            assert!(panel.is_zoomed(cx));
7094            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
7095        });
7096
7097        // Transferring focus back to the panel keeps it zoomed
7098        workspace.update(cx, |workspace, cx| {
7099            workspace.toggle_panel_focus::<TestPanel>(cx);
7100        });
7101
7102        workspace.update(cx, |workspace, cx| {
7103            assert!(workspace.right_dock().read(cx).is_open());
7104            assert!(panel.is_zoomed(cx));
7105            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7106        });
7107
7108        // Close the dock while it is zoomed
7109        workspace.update(cx, |workspace, cx| {
7110            workspace.toggle_dock(DockPosition::Right, cx)
7111        });
7112
7113        workspace.update(cx, |workspace, cx| {
7114            assert!(!workspace.right_dock().read(cx).is_open());
7115            assert!(panel.is_zoomed(cx));
7116            assert!(workspace.zoomed.is_none());
7117            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
7118        });
7119
7120        // Opening the dock, when it's zoomed, retains focus
7121        workspace.update(cx, |workspace, cx| {
7122            workspace.toggle_dock(DockPosition::Right, cx)
7123        });
7124
7125        workspace.update(cx, |workspace, cx| {
7126            assert!(workspace.right_dock().read(cx).is_open());
7127            assert!(panel.is_zoomed(cx));
7128            assert!(workspace.zoomed.is_some());
7129            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7130        });
7131
7132        // Unzoom and close the panel, zoom the active pane.
7133        panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
7134        workspace.update(cx, |workspace, cx| {
7135            workspace.toggle_dock(DockPosition::Right, cx)
7136        });
7137        pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
7138
7139        // Opening a dock unzooms the pane.
7140        workspace.update(cx, |workspace, cx| {
7141            workspace.toggle_dock(DockPosition::Right, cx)
7142        });
7143        workspace.update(cx, |workspace, cx| {
7144            let pane = pane.read(cx);
7145            assert!(!pane.is_zoomed());
7146            assert!(!pane.focus_handle(cx).is_focused(cx));
7147            assert!(workspace.right_dock().read(cx).is_open());
7148            assert!(workspace.zoomed.is_none());
7149        });
7150    }
7151
7152    #[gpui::test]
7153    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7154        init_test(cx);
7155
7156        let fs = FakeFs::new(cx.executor());
7157
7158        let project = Project::test(fs, None, cx).await;
7159        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7160
7161        // Let's arrange the panes like this:
7162        //
7163        // +-----------------------+
7164        // |         top           |
7165        // +------+--------+-------+
7166        // | left | center | right |
7167        // +------+--------+-------+
7168        // |        bottom         |
7169        // +-----------------------+
7170
7171        let top_item = cx.new_view(|cx| {
7172            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7173        });
7174        let bottom_item = cx.new_view(|cx| {
7175            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7176        });
7177        let left_item = cx.new_view(|cx| {
7178            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7179        });
7180        let right_item = cx.new_view(|cx| {
7181            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7182        });
7183        let center_item = cx.new_view(|cx| {
7184            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7185        });
7186
7187        let top_pane_id = workspace.update(cx, |workspace, cx| {
7188            let top_pane_id = workspace.active_pane().entity_id();
7189            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
7190            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
7191            top_pane_id
7192        });
7193        let bottom_pane_id = workspace.update(cx, |workspace, cx| {
7194            let bottom_pane_id = workspace.active_pane().entity_id();
7195            workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
7196            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
7197            bottom_pane_id
7198        });
7199        let left_pane_id = workspace.update(cx, |workspace, cx| {
7200            let left_pane_id = workspace.active_pane().entity_id();
7201            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
7202            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7203            left_pane_id
7204        });
7205        let right_pane_id = workspace.update(cx, |workspace, cx| {
7206            let right_pane_id = workspace.active_pane().entity_id();
7207            workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
7208            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
7209            right_pane_id
7210        });
7211        let center_pane_id = workspace.update(cx, |workspace, cx| {
7212            let center_pane_id = workspace.active_pane().entity_id();
7213            workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
7214            center_pane_id
7215        });
7216        cx.executor().run_until_parked();
7217
7218        workspace.update(cx, |workspace, cx| {
7219            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7220
7221            // Join into next from center pane into right
7222            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7223        });
7224
7225        workspace.update(cx, |workspace, cx| {
7226            let active_pane = workspace.active_pane();
7227            assert_eq!(right_pane_id, active_pane.entity_id());
7228            assert_eq!(2, active_pane.read(cx).items_len());
7229            let item_ids_in_pane =
7230                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7231            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7232            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7233
7234            // Join into next from right pane into bottom
7235            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7236        });
7237
7238        workspace.update(cx, |workspace, cx| {
7239            let active_pane = workspace.active_pane();
7240            assert_eq!(bottom_pane_id, active_pane.entity_id());
7241            assert_eq!(3, active_pane.read(cx).items_len());
7242            let item_ids_in_pane =
7243                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7244            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7245            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7246            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7247
7248            // Join into next from bottom pane into left
7249            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7250        });
7251
7252        workspace.update(cx, |workspace, cx| {
7253            let active_pane = workspace.active_pane();
7254            assert_eq!(left_pane_id, active_pane.entity_id());
7255            assert_eq!(4, active_pane.read(cx).items_len());
7256            let item_ids_in_pane =
7257                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7258            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7259            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7260            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7261            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7262
7263            // Join into next from left pane into top
7264            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7265        });
7266
7267        workspace.update(cx, |workspace, cx| {
7268            let active_pane = workspace.active_pane();
7269            assert_eq!(top_pane_id, active_pane.entity_id());
7270            assert_eq!(5, active_pane.read(cx).items_len());
7271            let item_ids_in_pane =
7272                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7273            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7274            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7275            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7276            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7277            assert!(item_ids_in_pane.contains(&top_item.item_id()));
7278
7279            // Single pane left: no-op
7280            workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
7281        });
7282
7283        workspace.update(cx, |workspace, _cx| {
7284            let active_pane = workspace.active_pane();
7285            assert_eq!(top_pane_id, active_pane.entity_id());
7286        });
7287    }
7288
7289    fn add_an_item_to_active_pane(
7290        cx: &mut VisualTestContext,
7291        workspace: &View<Workspace>,
7292        item_id: u64,
7293    ) -> View<TestItem> {
7294        let item = cx.new_view(|cx| {
7295            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7296                item_id,
7297                "item{item_id}.txt",
7298                cx,
7299            )])
7300        });
7301        workspace.update(cx, |workspace, cx| {
7302            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
7303        });
7304        return item;
7305    }
7306
7307    fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
7308        return workspace.update(cx, |workspace, cx| {
7309            let new_pane =
7310                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7311            new_pane
7312        });
7313    }
7314
7315    #[gpui::test]
7316    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7317        init_test(cx);
7318        let fs = FakeFs::new(cx.executor());
7319        let project = Project::test(fs, None, cx).await;
7320        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7321
7322        add_an_item_to_active_pane(cx, &workspace, 1);
7323        split_pane(cx, &workspace);
7324        add_an_item_to_active_pane(cx, &workspace, 2);
7325        split_pane(cx, &workspace); // empty pane
7326        split_pane(cx, &workspace);
7327        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7328
7329        cx.executor().run_until_parked();
7330
7331        workspace.update(cx, |workspace, cx| {
7332            let num_panes = workspace.panes().len();
7333            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7334            let active_item = workspace
7335                .active_pane()
7336                .read(cx)
7337                .active_item()
7338                .expect("item is in focus");
7339
7340            assert_eq!(num_panes, 4);
7341            assert_eq!(num_items_in_current_pane, 1);
7342            assert_eq!(active_item.item_id(), last_item.item_id());
7343        });
7344
7345        workspace.update(cx, |workspace, cx| {
7346            workspace.join_all_panes(cx);
7347        });
7348
7349        workspace.update(cx, |workspace, cx| {
7350            let num_panes = workspace.panes().len();
7351            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7352            let active_item = workspace
7353                .active_pane()
7354                .read(cx)
7355                .active_item()
7356                .expect("item is in focus");
7357
7358            assert_eq!(num_panes, 1);
7359            assert_eq!(num_items_in_current_pane, 3);
7360            assert_eq!(active_item.item_id(), last_item.item_id());
7361        });
7362    }
7363    struct TestModal(FocusHandle);
7364
7365    impl TestModal {
7366        fn new(cx: &mut ViewContext<Self>) -> Self {
7367            Self(cx.focus_handle())
7368        }
7369    }
7370
7371    impl EventEmitter<DismissEvent> for TestModal {}
7372
7373    impl FocusableView for TestModal {
7374        fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7375            self.0.clone()
7376        }
7377    }
7378
7379    impl ModalView for TestModal {}
7380
7381    impl Render for TestModal {
7382        fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
7383            div().track_focus(&self.0)
7384        }
7385    }
7386
7387    #[gpui::test]
7388    async fn test_panels(cx: &mut gpui::TestAppContext) {
7389        init_test(cx);
7390        let fs = FakeFs::new(cx.executor());
7391
7392        let project = Project::test(fs, [], cx).await;
7393        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7394
7395        let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7396            let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7397            workspace.add_panel(panel_1.clone(), cx);
7398            workspace.toggle_dock(DockPosition::Left, cx);
7399            let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7400            workspace.add_panel(panel_2.clone(), cx);
7401            workspace.toggle_dock(DockPosition::Right, cx);
7402
7403            let left_dock = workspace.left_dock();
7404            assert_eq!(
7405                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7406                panel_1.panel_id()
7407            );
7408            assert_eq!(
7409                left_dock.read(cx).active_panel_size(cx).unwrap(),
7410                panel_1.size(cx)
7411            );
7412
7413            left_dock.update(cx, |left_dock, cx| {
7414                left_dock.resize_active_panel(Some(px(1337.)), cx)
7415            });
7416            assert_eq!(
7417                workspace
7418                    .right_dock()
7419                    .read(cx)
7420                    .visible_panel()
7421                    .unwrap()
7422                    .panel_id(),
7423                panel_2.panel_id(),
7424            );
7425
7426            (panel_1, panel_2)
7427        });
7428
7429        // Move panel_1 to the right
7430        panel_1.update(cx, |panel_1, cx| {
7431            panel_1.set_position(DockPosition::Right, cx)
7432        });
7433
7434        workspace.update(cx, |workspace, cx| {
7435            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7436            // Since it was the only panel on the left, the left dock should now be closed.
7437            assert!(!workspace.left_dock().read(cx).is_open());
7438            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7439            let right_dock = workspace.right_dock();
7440            assert_eq!(
7441                right_dock.read(cx).visible_panel().unwrap().panel_id(),
7442                panel_1.panel_id()
7443            );
7444            assert_eq!(
7445                right_dock.read(cx).active_panel_size(cx).unwrap(),
7446                px(1337.)
7447            );
7448
7449            // Now we move panel_2 to the left
7450            panel_2.set_position(DockPosition::Left, cx);
7451        });
7452
7453        workspace.update(cx, |workspace, cx| {
7454            // Since panel_2 was not visible on the right, we don't open the left dock.
7455            assert!(!workspace.left_dock().read(cx).is_open());
7456            // And the right dock is unaffected in its displaying of panel_1
7457            assert!(workspace.right_dock().read(cx).is_open());
7458            assert_eq!(
7459                workspace
7460                    .right_dock()
7461                    .read(cx)
7462                    .visible_panel()
7463                    .unwrap()
7464                    .panel_id(),
7465                panel_1.panel_id(),
7466            );
7467        });
7468
7469        // Move panel_1 back to the left
7470        panel_1.update(cx, |panel_1, cx| {
7471            panel_1.set_position(DockPosition::Left, cx)
7472        });
7473
7474        workspace.update(cx, |workspace, cx| {
7475            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7476            let left_dock = workspace.left_dock();
7477            assert!(left_dock.read(cx).is_open());
7478            assert_eq!(
7479                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7480                panel_1.panel_id()
7481            );
7482            assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
7483            // And the right dock should be closed as it no longer has any panels.
7484            assert!(!workspace.right_dock().read(cx).is_open());
7485
7486            // Now we move panel_1 to the bottom
7487            panel_1.set_position(DockPosition::Bottom, cx);
7488        });
7489
7490        workspace.update(cx, |workspace, cx| {
7491            // Since panel_1 was visible on the left, we close the left dock.
7492            assert!(!workspace.left_dock().read(cx).is_open());
7493            // The bottom dock is sized based on the panel's default size,
7494            // since the panel orientation changed from vertical to horizontal.
7495            let bottom_dock = workspace.bottom_dock();
7496            assert_eq!(
7497                bottom_dock.read(cx).active_panel_size(cx).unwrap(),
7498                panel_1.size(cx),
7499            );
7500            // Close bottom dock and move panel_1 back to the left.
7501            bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
7502            panel_1.set_position(DockPosition::Left, cx);
7503        });
7504
7505        // Emit activated event on panel 1
7506        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7507
7508        // Now the left dock is open and panel_1 is active and focused.
7509        workspace.update(cx, |workspace, cx| {
7510            let left_dock = workspace.left_dock();
7511            assert!(left_dock.read(cx).is_open());
7512            assert_eq!(
7513                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7514                panel_1.panel_id(),
7515            );
7516            assert!(panel_1.focus_handle(cx).is_focused(cx));
7517        });
7518
7519        // Emit closed event on panel 2, which is not active
7520        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7521
7522        // Wo don't close the left dock, because panel_2 wasn't the active panel
7523        workspace.update(cx, |workspace, cx| {
7524            let left_dock = workspace.left_dock();
7525            assert!(left_dock.read(cx).is_open());
7526            assert_eq!(
7527                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7528                panel_1.panel_id(),
7529            );
7530        });
7531
7532        // Emitting a ZoomIn event shows the panel as zoomed.
7533        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7534        workspace.update(cx, |workspace, _| {
7535            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7536            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7537        });
7538
7539        // Move panel to another dock while it is zoomed
7540        panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
7541        workspace.update(cx, |workspace, _| {
7542            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7543
7544            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7545        });
7546
7547        // This is a helper for getting a:
7548        // - valid focus on an element,
7549        // - that isn't a part of the panes and panels system of the Workspace,
7550        // - and doesn't trigger the 'on_focus_lost' API.
7551        let focus_other_view = {
7552            let workspace = workspace.clone();
7553            move |cx: &mut VisualTestContext| {
7554                workspace.update(cx, |workspace, cx| {
7555                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7556                        workspace.toggle_modal(cx, TestModal::new);
7557                        workspace.toggle_modal(cx, TestModal::new);
7558                    } else {
7559                        workspace.toggle_modal(cx, TestModal::new);
7560                    }
7561                })
7562            }
7563        };
7564
7565        // If focus is transferred to another view that's not a panel or another pane, we still show
7566        // the panel as zoomed.
7567        focus_other_view(cx);
7568        workspace.update(cx, |workspace, _| {
7569            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7570            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7571        });
7572
7573        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7574        workspace.update(cx, |_, cx| cx.focus_self());
7575        workspace.update(cx, |workspace, _| {
7576            assert_eq!(workspace.zoomed, None);
7577            assert_eq!(workspace.zoomed_position, None);
7578        });
7579
7580        // If focus is transferred again to another view that's not a panel or a pane, we won't
7581        // show the panel as zoomed because it wasn't zoomed before.
7582        focus_other_view(cx);
7583        workspace.update(cx, |workspace, _| {
7584            assert_eq!(workspace.zoomed, None);
7585            assert_eq!(workspace.zoomed_position, None);
7586        });
7587
7588        // When the panel is activated, it is zoomed again.
7589        cx.dispatch_action(ToggleRightDock);
7590        workspace.update(cx, |workspace, _| {
7591            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7592            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7593        });
7594
7595        // Emitting a ZoomOut event unzooms the panel.
7596        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7597        workspace.update(cx, |workspace, _| {
7598            assert_eq!(workspace.zoomed, None);
7599            assert_eq!(workspace.zoomed_position, None);
7600        });
7601
7602        // Emit closed event on panel 1, which is active
7603        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7604
7605        // Now the left dock is closed, because panel_1 was the active panel
7606        workspace.update(cx, |workspace, cx| {
7607            let right_dock = workspace.right_dock();
7608            assert!(!right_dock.read(cx).is_open());
7609        });
7610    }
7611
7612    #[gpui::test]
7613    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
7614        init_test(cx);
7615
7616        let fs = FakeFs::new(cx.background_executor.clone());
7617        let project = Project::test(fs, [], cx).await;
7618        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7619        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7620
7621        let dirty_regular_buffer = cx.new_view(|cx| {
7622            TestItem::new(cx)
7623                .with_dirty(true)
7624                .with_label("1.txt")
7625                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7626        });
7627        let dirty_regular_buffer_2 = cx.new_view(|cx| {
7628            TestItem::new(cx)
7629                .with_dirty(true)
7630                .with_label("2.txt")
7631                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7632        });
7633        let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7634            TestItem::new(cx)
7635                .with_dirty(true)
7636                .with_singleton(false)
7637                .with_label("Fake Project Search")
7638                .with_project_items(&[
7639                    dirty_regular_buffer.read(cx).project_items[0].clone(),
7640                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7641                ])
7642        });
7643        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
7644        workspace.update(cx, |workspace, cx| {
7645            workspace.add_item(
7646                pane.clone(),
7647                Box::new(dirty_regular_buffer.clone()),
7648                None,
7649                false,
7650                false,
7651                cx,
7652            );
7653            workspace.add_item(
7654                pane.clone(),
7655                Box::new(dirty_regular_buffer_2.clone()),
7656                None,
7657                false,
7658                false,
7659                cx,
7660            );
7661            workspace.add_item(
7662                pane.clone(),
7663                Box::new(dirty_multi_buffer_with_both.clone()),
7664                None,
7665                false,
7666                false,
7667                cx,
7668            );
7669        });
7670
7671        pane.update(cx, |pane, cx| {
7672            pane.activate_item(2, true, true, cx);
7673            assert_eq!(
7674                pane.active_item().unwrap().item_id(),
7675                multi_buffer_with_both_files_id,
7676                "Should select the multi buffer in the pane"
7677            );
7678        });
7679        let close_all_but_multi_buffer_task = pane
7680            .update(cx, |pane, cx| {
7681                pane.close_inactive_items(
7682                    &CloseInactiveItems {
7683                        save_intent: Some(SaveIntent::Save),
7684                        close_pinned: true,
7685                    },
7686                    cx,
7687                )
7688            })
7689            .expect("should have inactive files to close");
7690        cx.background_executor.run_until_parked();
7691        assert!(
7692            !cx.has_pending_prompt(),
7693            "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown"
7694        );
7695        close_all_but_multi_buffer_task
7696            .await
7697            .expect("Closing all buffers but the multi buffer failed");
7698        pane.update(cx, |pane, cx| {
7699            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7700            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
7701            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7702            assert_eq!(pane.items_len(), 1);
7703            assert_eq!(
7704                pane.active_item().unwrap().item_id(),
7705                multi_buffer_with_both_files_id,
7706                "Should have only the multi buffer left in the pane"
7707            );
7708            assert!(
7709                dirty_multi_buffer_with_both.read(cx).is_dirty,
7710                "The multi buffer containing the unsaved buffer should still be dirty"
7711            );
7712        });
7713
7714        let close_multi_buffer_task = pane
7715            .update(cx, |pane, cx| {
7716                pane.close_active_item(
7717                    &CloseActiveItem {
7718                        save_intent: Some(SaveIntent::Close),
7719                    },
7720                    cx,
7721                )
7722            })
7723            .expect("should have the multi buffer to close");
7724        cx.background_executor.run_until_parked();
7725        assert!(
7726            cx.has_pending_prompt(),
7727            "Dirty multi buffer should prompt a save dialog"
7728        );
7729        cx.simulate_prompt_answer(0);
7730        cx.background_executor.run_until_parked();
7731        close_multi_buffer_task
7732            .await
7733            .expect("Closing the multi buffer failed");
7734        pane.update(cx, |pane, cx| {
7735            assert_eq!(
7736                dirty_multi_buffer_with_both.read(cx).save_count,
7737                1,
7738                "Multi buffer item should get be saved"
7739            );
7740            // Test impl does not save inner items, so we do not assert them
7741            assert_eq!(
7742                pane.items_len(),
7743                0,
7744                "No more items should be left in the pane"
7745            );
7746            assert!(pane.active_item().is_none());
7747        });
7748    }
7749
7750    #[gpui::test]
7751    async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane(
7752        cx: &mut TestAppContext,
7753    ) {
7754        init_test(cx);
7755
7756        let fs = FakeFs::new(cx.background_executor.clone());
7757        let project = Project::test(fs, [], cx).await;
7758        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7759        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7760
7761        let dirty_regular_buffer = cx.new_view(|cx| {
7762            TestItem::new(cx)
7763                .with_dirty(true)
7764                .with_label("1.txt")
7765                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7766        });
7767        let dirty_regular_buffer_2 = cx.new_view(|cx| {
7768            TestItem::new(cx)
7769                .with_dirty(true)
7770                .with_label("2.txt")
7771                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7772        });
7773        let clear_regular_buffer = cx.new_view(|cx| {
7774            TestItem::new(cx)
7775                .with_label("3.txt")
7776                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7777        });
7778
7779        let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7780            TestItem::new(cx)
7781                .with_dirty(true)
7782                .with_singleton(false)
7783                .with_label("Fake Project Search")
7784                .with_project_items(&[
7785                    dirty_regular_buffer.read(cx).project_items[0].clone(),
7786                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7787                    clear_regular_buffer.read(cx).project_items[0].clone(),
7788                ])
7789        });
7790        workspace.update(cx, |workspace, cx| {
7791            workspace.add_item(
7792                pane.clone(),
7793                Box::new(dirty_regular_buffer.clone()),
7794                None,
7795                false,
7796                false,
7797                cx,
7798            );
7799            workspace.add_item(
7800                pane.clone(),
7801                Box::new(dirty_multi_buffer_with_both.clone()),
7802                None,
7803                false,
7804                false,
7805                cx,
7806            );
7807        });
7808
7809        pane.update(cx, |pane, cx| {
7810            pane.activate_item(0, true, true, cx);
7811            assert_eq!(
7812                pane.active_item().unwrap().item_id(),
7813                dirty_regular_buffer.item_id(),
7814                "Should select the dirty singleton buffer in the pane"
7815            );
7816        });
7817        let close_singleton_buffer_task = pane
7818            .update(cx, |pane, cx| {
7819                pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7820            })
7821            .expect("should have active singleton buffer to close");
7822        cx.background_executor.run_until_parked();
7823        assert!(
7824            !cx.has_pending_prompt(),
7825            "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown"
7826        );
7827
7828        close_singleton_buffer_task
7829            .await
7830            .expect("Should not fail closing the singleton buffer");
7831        pane.update(cx, |pane, cx| {
7832            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7833            assert_eq!(
7834                dirty_multi_buffer_with_both.read(cx).save_count,
7835                0,
7836                "Multi buffer itself should not be saved"
7837            );
7838            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7839            assert_eq!(
7840                pane.items_len(),
7841                1,
7842                "A dirty multi buffer should be present in the pane"
7843            );
7844            assert_eq!(
7845                pane.active_item().unwrap().item_id(),
7846                dirty_multi_buffer_with_both.item_id(),
7847                "Should activate the only remaining item in the pane"
7848            );
7849        });
7850    }
7851
7852    #[gpui::test]
7853    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
7854        cx: &mut TestAppContext,
7855    ) {
7856        init_test(cx);
7857
7858        let fs = FakeFs::new(cx.background_executor.clone());
7859        let project = Project::test(fs, [], cx).await;
7860        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7861        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7862
7863        let dirty_regular_buffer = cx.new_view(|cx| {
7864            TestItem::new(cx)
7865                .with_dirty(true)
7866                .with_label("1.txt")
7867                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7868        });
7869        let dirty_regular_buffer_2 = cx.new_view(|cx| {
7870            TestItem::new(cx)
7871                .with_dirty(true)
7872                .with_label("2.txt")
7873                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7874        });
7875        let clear_regular_buffer = cx.new_view(|cx| {
7876            TestItem::new(cx)
7877                .with_label("3.txt")
7878                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7879        });
7880
7881        let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7882            TestItem::new(cx)
7883                .with_dirty(true)
7884                .with_singleton(false)
7885                .with_label("Fake Project Search")
7886                .with_project_items(&[
7887                    dirty_regular_buffer.read(cx).project_items[0].clone(),
7888                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7889                    clear_regular_buffer.read(cx).project_items[0].clone(),
7890                ])
7891        });
7892        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
7893        workspace.update(cx, |workspace, cx| {
7894            workspace.add_item(
7895                pane.clone(),
7896                Box::new(dirty_regular_buffer.clone()),
7897                None,
7898                false,
7899                false,
7900                cx,
7901            );
7902            workspace.add_item(
7903                pane.clone(),
7904                Box::new(dirty_multi_buffer_with_both.clone()),
7905                None,
7906                false,
7907                false,
7908                cx,
7909            );
7910        });
7911
7912        pane.update(cx, |pane, cx| {
7913            pane.activate_item(1, true, true, cx);
7914            assert_eq!(
7915                pane.active_item().unwrap().item_id(),
7916                multi_buffer_with_both_files_id,
7917                "Should select the multi buffer in the pane"
7918            );
7919        });
7920        let _close_multi_buffer_task = pane
7921            .update(cx, |pane, cx| {
7922                pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7923            })
7924            .expect("should have active multi buffer to close");
7925        cx.background_executor.run_until_parked();
7926        assert!(
7927            cx.has_pending_prompt(),
7928            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
7929        );
7930    }
7931
7932    #[gpui::test]
7933    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
7934        cx: &mut TestAppContext,
7935    ) {
7936        init_test(cx);
7937
7938        let fs = FakeFs::new(cx.background_executor.clone());
7939        let project = Project::test(fs, [], cx).await;
7940        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7941        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7942
7943        let dirty_regular_buffer = cx.new_view(|cx| {
7944            TestItem::new(cx)
7945                .with_dirty(true)
7946                .with_label("1.txt")
7947                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7948        });
7949        let dirty_regular_buffer_2 = cx.new_view(|cx| {
7950            TestItem::new(cx)
7951                .with_dirty(true)
7952                .with_label("2.txt")
7953                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7954        });
7955        let clear_regular_buffer = cx.new_view(|cx| {
7956            TestItem::new(cx)
7957                .with_label("3.txt")
7958                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7959        });
7960
7961        let dirty_multi_buffer = cx.new_view(|cx| {
7962            TestItem::new(cx)
7963                .with_dirty(true)
7964                .with_singleton(false)
7965                .with_label("Fake Project Search")
7966                .with_project_items(&[
7967                    dirty_regular_buffer.read(cx).project_items[0].clone(),
7968                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7969                    clear_regular_buffer.read(cx).project_items[0].clone(),
7970                ])
7971        });
7972        workspace.update(cx, |workspace, cx| {
7973            workspace.add_item(
7974                pane.clone(),
7975                Box::new(dirty_regular_buffer.clone()),
7976                None,
7977                false,
7978                false,
7979                cx,
7980            );
7981            workspace.add_item(
7982                pane.clone(),
7983                Box::new(dirty_regular_buffer_2.clone()),
7984                None,
7985                false,
7986                false,
7987                cx,
7988            );
7989            workspace.add_item(
7990                pane.clone(),
7991                Box::new(dirty_multi_buffer.clone()),
7992                None,
7993                false,
7994                false,
7995                cx,
7996            );
7997        });
7998
7999        pane.update(cx, |pane, cx| {
8000            pane.activate_item(2, true, true, cx);
8001            assert_eq!(
8002                pane.active_item().unwrap().item_id(),
8003                dirty_multi_buffer.item_id(),
8004                "Should select the multi buffer in the pane"
8005            );
8006        });
8007        let close_multi_buffer_task = pane
8008            .update(cx, |pane, cx| {
8009                pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
8010            })
8011            .expect("should have active multi buffer to close");
8012        cx.background_executor.run_until_parked();
8013        assert!(
8014            !cx.has_pending_prompt(),
8015            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8016        );
8017        close_multi_buffer_task
8018            .await
8019            .expect("Closing multi buffer failed");
8020        pane.update(cx, |pane, cx| {
8021            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8022            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8023            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8024            assert_eq!(
8025                pane.items()
8026                    .map(|item| item.item_id())
8027                    .sorted()
8028                    .collect::<Vec<_>>(),
8029                vec![
8030                    dirty_regular_buffer.item_id(),
8031                    dirty_regular_buffer_2.item_id(),
8032                ],
8033                "Should have no multi buffer left in the pane"
8034            );
8035            assert!(dirty_regular_buffer.read(cx).is_dirty);
8036            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8037        });
8038    }
8039
8040    #[gpui::test]
8041    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8042        init_test(cx);
8043        let fs = FakeFs::new(cx.executor());
8044        let project = Project::test(fs, [], cx).await;
8045        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
8046
8047        // Add a new panel to the right dock, opening the dock and setting the
8048        // focus to the new panel.
8049        let panel = workspace.update(cx, |workspace, cx| {
8050            let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
8051            workspace.add_panel(panel.clone(), cx);
8052
8053            workspace
8054                .right_dock()
8055                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
8056
8057            workspace.toggle_panel_focus::<TestPanel>(cx);
8058
8059            panel
8060        });
8061
8062        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8063        // panel to the next valid position which, in this case, is the left
8064        // dock.
8065        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8066        workspace.update(cx, |workspace, cx| {
8067            assert!(workspace.left_dock().read(cx).is_open());
8068            assert_eq!(panel.read(cx).position, DockPosition::Left);
8069        });
8070
8071        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8072        // panel to the next valid position which, in this case, is the bottom
8073        // dock.
8074        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8075        workspace.update(cx, |workspace, cx| {
8076            assert!(workspace.bottom_dock().read(cx).is_open());
8077            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8078        });
8079
8080        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8081        // around moving the panel to its initial position, the right dock.
8082        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8083        workspace.update(cx, |workspace, cx| {
8084            assert!(workspace.right_dock().read(cx).is_open());
8085            assert_eq!(panel.read(cx).position, DockPosition::Right);
8086        });
8087
8088        // Remove focus from the panel, ensuring that, if the panel is not
8089        // focused, the `MoveFocusedPanelToNextPosition` action does not update
8090        // the panel's position, so the panel is still in the right dock.
8091        workspace.update(cx, |workspace, cx| {
8092            workspace.toggle_panel_focus::<TestPanel>(cx);
8093        });
8094
8095        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8096        workspace.update(cx, |workspace, cx| {
8097            assert!(workspace.right_dock().read(cx).is_open());
8098            assert_eq!(panel.read(cx).position, DockPosition::Right);
8099        });
8100    }
8101
8102    mod register_project_item_tests {
8103        use gpui::Context as _;
8104
8105        use super::*;
8106
8107        // View
8108        struct TestPngItemView {
8109            focus_handle: FocusHandle,
8110        }
8111        // Model
8112        struct TestPngItem {}
8113
8114        impl project::ProjectItem for TestPngItem {
8115            fn try_open(
8116                _project: &Model<Project>,
8117                path: &ProjectPath,
8118                cx: &mut AppContext,
8119            ) -> Option<Task<gpui::Result<Model<Self>>>> {
8120                if path.path.extension().unwrap() == "png" {
8121                    Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
8122                } else {
8123                    None
8124                }
8125            }
8126
8127            fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
8128                None
8129            }
8130
8131            fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
8132                None
8133            }
8134
8135            fn is_dirty(&self) -> bool {
8136                false
8137            }
8138        }
8139
8140        impl Item for TestPngItemView {
8141            type Event = ();
8142        }
8143        impl EventEmitter<()> for TestPngItemView {}
8144        impl FocusableView for TestPngItemView {
8145            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
8146                self.focus_handle.clone()
8147            }
8148        }
8149
8150        impl Render for TestPngItemView {
8151            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
8152                Empty
8153            }
8154        }
8155
8156        impl ProjectItem for TestPngItemView {
8157            type Item = TestPngItem;
8158
8159            fn for_project_item(
8160                _project: Model<Project>,
8161                _item: Model<Self::Item>,
8162                cx: &mut ViewContext<Self>,
8163            ) -> Self
8164            where
8165                Self: Sized,
8166            {
8167                Self {
8168                    focus_handle: cx.focus_handle(),
8169                }
8170            }
8171        }
8172
8173        // View
8174        struct TestIpynbItemView {
8175            focus_handle: FocusHandle,
8176        }
8177        // Model
8178        struct TestIpynbItem {}
8179
8180        impl project::ProjectItem for TestIpynbItem {
8181            fn try_open(
8182                _project: &Model<Project>,
8183                path: &ProjectPath,
8184                cx: &mut AppContext,
8185            ) -> Option<Task<gpui::Result<Model<Self>>>> {
8186                if path.path.extension().unwrap() == "ipynb" {
8187                    Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
8188                } else {
8189                    None
8190                }
8191            }
8192
8193            fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
8194                None
8195            }
8196
8197            fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
8198                None
8199            }
8200
8201            fn is_dirty(&self) -> bool {
8202                false
8203            }
8204        }
8205
8206        impl Item for TestIpynbItemView {
8207            type Event = ();
8208        }
8209        impl EventEmitter<()> for TestIpynbItemView {}
8210        impl FocusableView for TestIpynbItemView {
8211            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
8212                self.focus_handle.clone()
8213            }
8214        }
8215
8216        impl Render for TestIpynbItemView {
8217            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
8218                Empty
8219            }
8220        }
8221
8222        impl ProjectItem for TestIpynbItemView {
8223            type Item = TestIpynbItem;
8224
8225            fn for_project_item(
8226                _project: Model<Project>,
8227                _item: Model<Self::Item>,
8228                cx: &mut ViewContext<Self>,
8229            ) -> Self
8230            where
8231                Self: Sized,
8232            {
8233                Self {
8234                    focus_handle: cx.focus_handle(),
8235                }
8236            }
8237        }
8238
8239        struct TestAlternatePngItemView {
8240            focus_handle: FocusHandle,
8241        }
8242
8243        impl Item for TestAlternatePngItemView {
8244            type Event = ();
8245        }
8246
8247        impl EventEmitter<()> for TestAlternatePngItemView {}
8248        impl FocusableView for TestAlternatePngItemView {
8249            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
8250                self.focus_handle.clone()
8251            }
8252        }
8253
8254        impl Render for TestAlternatePngItemView {
8255            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
8256                Empty
8257            }
8258        }
8259
8260        impl ProjectItem for TestAlternatePngItemView {
8261            type Item = TestPngItem;
8262
8263            fn for_project_item(
8264                _project: Model<Project>,
8265                _item: Model<Self::Item>,
8266                cx: &mut ViewContext<Self>,
8267            ) -> Self
8268            where
8269                Self: Sized,
8270            {
8271                Self {
8272                    focus_handle: cx.focus_handle(),
8273                }
8274            }
8275        }
8276
8277        #[gpui::test]
8278        async fn test_register_project_item(cx: &mut TestAppContext) {
8279            init_test(cx);
8280
8281            cx.update(|cx| {
8282                register_project_item::<TestPngItemView>(cx);
8283                register_project_item::<TestIpynbItemView>(cx);
8284            });
8285
8286            let fs = FakeFs::new(cx.executor());
8287            fs.insert_tree(
8288                "/root1",
8289                json!({
8290                    "one.png": "BINARYDATAHERE",
8291                    "two.ipynb": "{ totally a notebook }",
8292                    "three.txt": "editing text, sure why not?"
8293                }),
8294            )
8295            .await;
8296
8297            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8298            let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
8299
8300            let worktree_id = project.update(cx, |project, cx| {
8301                project.worktrees(cx).next().unwrap().read(cx).id()
8302            });
8303
8304            let handle = workspace
8305                .update(cx, |workspace, cx| {
8306                    let project_path = (worktree_id, "one.png");
8307                    workspace.open_path(project_path, None, true, cx)
8308                })
8309                .await
8310                .unwrap();
8311
8312            // Now we can check if the handle we got back errored or not
8313            assert_eq!(
8314                handle.to_any().entity_type(),
8315                TypeId::of::<TestPngItemView>()
8316            );
8317
8318            let handle = workspace
8319                .update(cx, |workspace, cx| {
8320                    let project_path = (worktree_id, "two.ipynb");
8321                    workspace.open_path(project_path, None, true, cx)
8322                })
8323                .await
8324                .unwrap();
8325
8326            assert_eq!(
8327                handle.to_any().entity_type(),
8328                TypeId::of::<TestIpynbItemView>()
8329            );
8330
8331            let handle = workspace
8332                .update(cx, |workspace, cx| {
8333                    let project_path = (worktree_id, "three.txt");
8334                    workspace.open_path(project_path, None, true, cx)
8335                })
8336                .await;
8337            assert!(handle.is_err());
8338        }
8339
8340        #[gpui::test]
8341        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8342            init_test(cx);
8343
8344            cx.update(|cx| {
8345                register_project_item::<TestPngItemView>(cx);
8346                register_project_item::<TestAlternatePngItemView>(cx);
8347            });
8348
8349            let fs = FakeFs::new(cx.executor());
8350            fs.insert_tree(
8351                "/root1",
8352                json!({
8353                    "one.png": "BINARYDATAHERE",
8354                    "two.ipynb": "{ totally a notebook }",
8355                    "three.txt": "editing text, sure why not?"
8356                }),
8357            )
8358            .await;
8359
8360            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8361            let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
8362
8363            let worktree_id = project.update(cx, |project, cx| {
8364                project.worktrees(cx).next().unwrap().read(cx).id()
8365            });
8366
8367            let handle = workspace
8368                .update(cx, |workspace, cx| {
8369                    let project_path = (worktree_id, "one.png");
8370                    workspace.open_path(project_path, None, true, cx)
8371                })
8372                .await
8373                .unwrap();
8374
8375            // This _must_ be the second item registered
8376            assert_eq!(
8377                handle.to_any().entity_type(),
8378                TypeId::of::<TestAlternatePngItemView>()
8379            );
8380
8381            let handle = workspace
8382                .update(cx, |workspace, cx| {
8383                    let project_path = (worktree_id, "three.txt");
8384                    workspace.open_path(project_path, None, true, cx)
8385                })
8386                .await;
8387            assert!(handle.is_err());
8388        }
8389    }
8390
8391    pub fn init_test(cx: &mut TestAppContext) {
8392        cx.update(|cx| {
8393            let settings_store = SettingsStore::test(cx);
8394            cx.set_global(settings_store);
8395            theme::init(theme::LoadThemes::JustBase, cx);
8396            language::init(cx);
8397            crate::init_settings(cx);
8398            Project::init_settings(cx);
8399        });
8400    }
8401
8402    fn dirty_project_item(id: u64, path: &str, cx: &mut AppContext) -> Model<TestProjectItem> {
8403        let item = TestProjectItem::new(id, path, cx);
8404        item.update(cx, |item, _| {
8405            item.is_dirty = true;
8406        });
8407        item
8408    }
8409}