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