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
 105static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
 106    env::var("ZED_WINDOW_SIZE")
 107        .ok()
 108        .as_deref()
 109        .and_then(parse_pixel_size_env_var)
 110});
 111
 112static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
 113    env::var("ZED_WINDOW_POSITION")
 114        .ok()
 115        .as_deref()
 116        .and_then(parse_pixel_position_env_var)
 117});
 118
 119actions!(assistant, [ShowConfiguration]);
 120
 121actions!(
 122    workspace,
 123    [
 124        ActivateNextPane,
 125        ActivatePreviousPane,
 126        ActivateNextWindow,
 127        ActivatePreviousWindow,
 128        AddFolderToProject,
 129        ClearAllNotifications,
 130        CloseAllDocks,
 131        CloseWindow,
 132        CopyPath,
 133        CopyRelativePath,
 134        Feedback,
 135        FollowNextCollaborator,
 136        MoveFocusedPanelToNextPosition,
 137        NewCenterTerminal,
 138        NewFile,
 139        NewFileSplitVertical,
 140        NewFileSplitHorizontal,
 141        NewSearch,
 142        NewTerminal,
 143        NewWindow,
 144        Open,
 145        OpenFiles,
 146        OpenInTerminal,
 147        ReloadActiveItem,
 148        SaveAs,
 149        SaveWithoutFormat,
 150        ToggleBottomDock,
 151        ToggleCenteredLayout,
 152        ToggleLeftDock,
 153        ToggleRightDock,
 154        ToggleZoom,
 155        Unfollow,
 156        Welcome,
 157    ]
 158);
 159
 160#[derive(Clone, PartialEq)]
 161pub struct OpenPaths {
 162    pub paths: Vec<PathBuf>,
 163}
 164
 165#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 166pub struct ActivatePane(pub usize);
 167
 168#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 169pub struct ActivatePaneInDirection(pub SplitDirection);
 170
 171#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 172pub struct SwapPaneInDirection(pub SplitDirection);
 173
 174#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 175pub struct MoveItemToPane {
 176    pub destination: usize,
 177    #[serde(default = "default_true")]
 178    pub focus: bool,
 179}
 180
 181#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 182pub struct MoveItemToPaneInDirection {
 183    pub direction: SplitDirection,
 184    #[serde(default = "default_true")]
 185    pub focus: bool,
 186}
 187
 188#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
 189#[serde(rename_all = "camelCase")]
 190pub struct SaveAll {
 191    pub save_intent: Option<SaveIntent>,
 192}
 193
 194#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
 195#[serde(rename_all = "camelCase")]
 196pub struct Save {
 197    pub save_intent: Option<SaveIntent>,
 198}
 199
 200#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
 201#[serde(rename_all = "camelCase")]
 202pub struct CloseAllItemsAndPanes {
 203    pub save_intent: Option<SaveIntent>,
 204}
 205
 206#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
 207#[serde(rename_all = "camelCase")]
 208pub struct CloseInactiveTabsAndPanes {
 209    pub save_intent: Option<SaveIntent>,
 210}
 211
 212#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 213pub struct SendKeystrokes(pub String);
 214
 215#[derive(Clone, Deserialize, PartialEq, Default, JsonSchema)]
 216pub struct Reload {
 217    pub binary_path: Option<PathBuf>,
 218}
 219
 220action_as!(project_symbols, ToggleProjectSymbols as Toggle);
 221
 222#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema)]
 223pub struct ToggleFileFinder {
 224    #[serde(default)]
 225    pub separate_history: bool,
 226}
 227
 228impl_action_as!(file_finder, ToggleFileFinder as Toggle);
 229
 230impl_actions!(
 231    workspace,
 232    [
 233        ActivatePane,
 234        ActivatePaneInDirection,
 235        CloseAllItemsAndPanes,
 236        CloseInactiveTabsAndPanes,
 237        MoveItemToPane,
 238        MoveItemToPaneInDirection,
 239        OpenTerminal,
 240        Reload,
 241        Save,
 242        SaveAll,
 243        SwapPaneInDirection,
 244        SendKeystrokes,
 245    ]
 246);
 247
 248#[derive(PartialEq, Eq, Debug)]
 249pub enum CloseIntent {
 250    /// Quit the program entirely.
 251    Quit,
 252    /// Close a window.
 253    CloseWindow,
 254    /// Replace the workspace in an existing window.
 255    ReplaceWindow,
 256}
 257
 258#[derive(Clone)]
 259pub struct Toast {
 260    id: NotificationId,
 261    msg: Cow<'static, str>,
 262    autohide: bool,
 263    on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
 264}
 265
 266impl Toast {
 267    pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
 268        Toast {
 269            id,
 270            msg: msg.into(),
 271            on_click: None,
 272            autohide: false,
 273        }
 274    }
 275
 276    pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
 277    where
 278        M: Into<Cow<'static, str>>,
 279        F: Fn(&mut WindowContext) + 'static,
 280    {
 281        self.on_click = Some((message.into(), Arc::new(on_click)));
 282        self
 283    }
 284
 285    pub fn autohide(mut self) -> Self {
 286        self.autohide = true;
 287        self
 288    }
 289}
 290
 291impl PartialEq for Toast {
 292    fn eq(&self, other: &Self) -> bool {
 293        self.id == other.id
 294            && self.msg == other.msg
 295            && self.on_click.is_some() == other.on_click.is_some()
 296    }
 297}
 298
 299#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema)]
 300pub struct OpenTerminal {
 301    pub working_directory: PathBuf,
 302}
 303
 304#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
 305pub struct WorkspaceId(i64);
 306
 307impl StaticColumnCount for WorkspaceId {}
 308impl Bind for WorkspaceId {
 309    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
 310        self.0.bind(statement, start_index)
 311    }
 312}
 313impl Column for WorkspaceId {
 314    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 315        i64::column(statement, start_index)
 316            .map(|(i, next_index)| (Self(i), next_index))
 317            .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
 318    }
 319}
 320impl From<WorkspaceId> for i64 {
 321    fn from(val: WorkspaceId) -> Self {
 322        val.0
 323    }
 324}
 325
 326pub fn init_settings(cx: &mut AppContext) {
 327    WorkspaceSettings::register(cx);
 328    ItemSettings::register(cx);
 329    PreviewTabsSettings::register(cx);
 330    TabBarSettings::register(cx);
 331}
 332
 333fn prompt_and_open_paths(
 334    app_state: Arc<AppState>,
 335    options: PathPromptOptions,
 336    cx: &mut AppContext,
 337) {
 338    let paths = cx.prompt_for_paths(options);
 339    cx.spawn(|cx| async move {
 340        match paths.await.anyhow().and_then(|res| res) {
 341            Ok(Some(paths)) => {
 342                cx.update(|cx| {
 343                    open_paths(&paths, app_state, OpenOptions::default(), cx).detach_and_log_err(cx)
 344                })
 345                .ok();
 346            }
 347            Ok(None) => {}
 348            Err(err) => {
 349                util::log_err(&err);
 350                cx.update(|cx| {
 351                    if let Some(workspace_window) = cx
 352                        .active_window()
 353                        .and_then(|window| window.downcast::<Workspace>())
 354                    {
 355                        workspace_window
 356                            .update(cx, |workspace, cx| {
 357                                workspace.show_portal_error(err.to_string(), cx);
 358                            })
 359                            .ok();
 360                    }
 361                })
 362                .ok();
 363            }
 364        }
 365    })
 366    .detach();
 367}
 368
 369pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
 370    init_settings(cx);
 371    notifications::init(cx);
 372    theme_preview::init(cx);
 373
 374    cx.on_action(Workspace::close_global);
 375    cx.on_action(reload);
 376
 377    cx.on_action({
 378        let app_state = Arc::downgrade(&app_state);
 379        move |_: &Open, cx: &mut AppContext| {
 380            if let Some(app_state) = app_state.upgrade() {
 381                prompt_and_open_paths(
 382                    app_state,
 383                    PathPromptOptions {
 384                        files: true,
 385                        directories: true,
 386                        multiple: true,
 387                    },
 388                    cx,
 389                );
 390            }
 391        }
 392    });
 393    cx.on_action({
 394        let app_state = Arc::downgrade(&app_state);
 395        move |_: &OpenFiles, cx: &mut AppContext| {
 396            let directories = cx.can_select_mixed_files_and_dirs();
 397            if let Some(app_state) = app_state.upgrade() {
 398                prompt_and_open_paths(
 399                    app_state,
 400                    PathPromptOptions {
 401                        files: true,
 402                        directories,
 403                        multiple: true,
 404                    },
 405                    cx,
 406                );
 407            }
 408        }
 409    });
 410}
 411
 412#[derive(Clone, Default, Deref, DerefMut)]
 413struct ProjectItemOpeners(Vec<ProjectItemOpener>);
 414
 415type ProjectItemOpener = fn(
 416    &Model<Project>,
 417    &ProjectPath,
 418    &mut WindowContext,
 419)
 420    -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
 421
 422type WorkspaceItemBuilder = Box<dyn FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
 423
 424impl Global for ProjectItemOpeners {}
 425
 426/// Registers a [ProjectItem] for the app. When opening a file, all the registered
 427/// items will get a chance to open the file, starting from the project item that
 428/// was added last.
 429pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
 430    let builders = cx.default_global::<ProjectItemOpeners>();
 431    builders.push(|project, project_path, cx| {
 432        let project_item = <I::Item as project::ProjectItem>::try_open(project, project_path, cx)?;
 433        let project = project.clone();
 434        Some(cx.spawn(|cx| async move {
 435            let project_item = project_item.await?;
 436            let project_entry_id: Option<ProjectEntryId> =
 437                project_item.read_with(&cx, project::ProjectItem::entry_id)?;
 438            let build_workspace_item = Box::new(|cx: &mut ViewContext<Pane>| {
 439                Box::new(cx.new_view(|cx| I::for_project_item(project, project_item, cx)))
 440                    as Box<dyn ItemHandle>
 441            }) as Box<_>;
 442            Ok((project_entry_id, build_workspace_item))
 443        }))
 444    });
 445}
 446
 447#[derive(Default)]
 448pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
 449
 450struct FollowableViewDescriptor {
 451    from_state_proto: fn(
 452        View<Workspace>,
 453        ViewId,
 454        &mut Option<proto::view::Variant>,
 455        &mut WindowContext,
 456    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
 457    to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
 458}
 459
 460impl Global for FollowableViewRegistry {}
 461
 462impl FollowableViewRegistry {
 463    pub fn register<I: FollowableItem>(cx: &mut AppContext) {
 464        cx.default_global::<Self>().0.insert(
 465            TypeId::of::<I>(),
 466            FollowableViewDescriptor {
 467                from_state_proto: |workspace, id, state, cx| {
 468                    I::from_state_proto(workspace, id, state, cx).map(|task| {
 469                        cx.foreground_executor()
 470                            .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
 471                    })
 472                },
 473                to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
 474            },
 475        );
 476    }
 477
 478    pub fn from_state_proto(
 479        workspace: View<Workspace>,
 480        view_id: ViewId,
 481        mut state: Option<proto::view::Variant>,
 482        cx: &mut WindowContext,
 483    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
 484        cx.update_default_global(|this: &mut Self, cx| {
 485            this.0.values().find_map(|descriptor| {
 486                (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, cx)
 487            })
 488        })
 489    }
 490
 491    pub fn to_followable_view(
 492        view: impl Into<AnyView>,
 493        cx: &AppContext,
 494    ) -> Option<Box<dyn FollowableItemHandle>> {
 495        let this = cx.try_global::<Self>()?;
 496        let view = view.into();
 497        let descriptor = this.0.get(&view.entity_type())?;
 498        Some((descriptor.to_followable_view)(&view))
 499    }
 500}
 501
 502#[derive(Copy, Clone)]
 503struct SerializableItemDescriptor {
 504    deserialize: fn(
 505        Model<Project>,
 506        WeakView<Workspace>,
 507        WorkspaceId,
 508        ItemId,
 509        &mut ViewContext<Pane>,
 510    ) -> Task<Result<Box<dyn ItemHandle>>>,
 511    cleanup: fn(WorkspaceId, Vec<ItemId>, &mut WindowContext) -> Task<Result<()>>,
 512    view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
 513}
 514
 515#[derive(Default)]
 516struct SerializableItemRegistry {
 517    descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
 518    descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
 519}
 520
 521impl Global for SerializableItemRegistry {}
 522
 523impl SerializableItemRegistry {
 524    fn deserialize(
 525        item_kind: &str,
 526        project: Model<Project>,
 527        workspace: WeakView<Workspace>,
 528        workspace_id: WorkspaceId,
 529        item_item: ItemId,
 530        cx: &mut ViewContext<Pane>,
 531    ) -> Task<Result<Box<dyn ItemHandle>>> {
 532        let Some(descriptor) = Self::descriptor(item_kind, cx) else {
 533            return Task::ready(Err(anyhow!(
 534                "cannot deserialize {}, descriptor not found",
 535                item_kind
 536            )));
 537        };
 538
 539        (descriptor.deserialize)(project, workspace, workspace_id, item_item, cx)
 540    }
 541
 542    fn cleanup(
 543        item_kind: &str,
 544        workspace_id: WorkspaceId,
 545        loaded_items: Vec<ItemId>,
 546        cx: &mut WindowContext,
 547    ) -> Task<Result<()>> {
 548        let Some(descriptor) = Self::descriptor(item_kind, cx) else {
 549            return Task::ready(Err(anyhow!(
 550                "cannot cleanup {}, descriptor not found",
 551                item_kind
 552            )));
 553        };
 554
 555        (descriptor.cleanup)(workspace_id, loaded_items, cx)
 556    }
 557
 558    fn view_to_serializable_item_handle(
 559        view: AnyView,
 560        cx: &AppContext,
 561    ) -> Option<Box<dyn SerializableItemHandle>> {
 562        let this = cx.try_global::<Self>()?;
 563        let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
 564        Some((descriptor.view_to_serializable_item)(view))
 565    }
 566
 567    fn descriptor(item_kind: &str, cx: &AppContext) -> Option<SerializableItemDescriptor> {
 568        let this = cx.try_global::<Self>()?;
 569        this.descriptors_by_kind.get(item_kind).copied()
 570    }
 571}
 572
 573pub fn register_serializable_item<I: SerializableItem>(cx: &mut AppContext) {
 574    let serialized_item_kind = I::serialized_item_kind();
 575
 576    let registry = cx.default_global::<SerializableItemRegistry>();
 577    let descriptor = SerializableItemDescriptor {
 578        deserialize: |project, workspace, workspace_id, item_id, cx| {
 579            let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
 580            cx.foreground_executor()
 581                .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
 582        },
 583        cleanup: |workspace_id, loaded_items, cx| I::cleanup(workspace_id, loaded_items, cx),
 584        view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
 585    };
 586    registry
 587        .descriptors_by_kind
 588        .insert(Arc::from(serialized_item_kind), descriptor);
 589    registry
 590        .descriptors_by_type
 591        .insert(TypeId::of::<I>(), descriptor);
 592}
 593
 594pub struct AppState {
 595    pub languages: Arc<LanguageRegistry>,
 596    pub client: Arc<Client>,
 597    pub user_store: Model<UserStore>,
 598    pub workspace_store: Model<WorkspaceStore>,
 599    pub fs: Arc<dyn fs::Fs>,
 600    pub build_window_options: fn(Option<Uuid>, &mut AppContext) -> WindowOptions,
 601    pub node_runtime: NodeRuntime,
 602    pub session: Model<AppSession>,
 603}
 604
 605struct GlobalAppState(Weak<AppState>);
 606
 607impl Global for GlobalAppState {}
 608
 609pub struct WorkspaceStore {
 610    workspaces: HashSet<WindowHandle<Workspace>>,
 611    client: Arc<Client>,
 612    _subscriptions: Vec<client::Subscription>,
 613}
 614
 615#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
 616struct Follower {
 617    project_id: Option<u64>,
 618    peer_id: PeerId,
 619}
 620
 621impl AppState {
 622    pub fn global(cx: &AppContext) -> Weak<Self> {
 623        cx.global::<GlobalAppState>().0.clone()
 624    }
 625    pub fn try_global(cx: &AppContext) -> Option<Weak<Self>> {
 626        cx.try_global::<GlobalAppState>()
 627            .map(|state| state.0.clone())
 628    }
 629    pub fn set_global(state: Weak<AppState>, cx: &mut AppContext) {
 630        cx.set_global(GlobalAppState(state));
 631    }
 632
 633    #[cfg(any(test, feature = "test-support"))]
 634    pub fn test(cx: &mut AppContext) -> Arc<Self> {
 635        use node_runtime::NodeRuntime;
 636        use session::Session;
 637        use settings::SettingsStore;
 638
 639        if !cx.has_global::<SettingsStore>() {
 640            let settings_store = SettingsStore::test(cx);
 641            cx.set_global(settings_store);
 642        }
 643
 644        let fs = fs::FakeFs::new(cx.background_executor().clone());
 645        let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
 646        let clock = Arc::new(clock::FakeSystemClock::new());
 647        let http_client = http_client::FakeHttpClient::with_404_response();
 648        let client = Client::new(clock, http_client.clone(), cx);
 649        let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
 650        let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
 651        let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
 652
 653        theme::init(theme::LoadThemes::JustBase, cx);
 654        client::init(&client, cx);
 655        crate::init_settings(cx);
 656
 657        Arc::new(Self {
 658            client,
 659            fs,
 660            languages,
 661            user_store,
 662            workspace_store,
 663            node_runtime: NodeRuntime::unavailable(),
 664            build_window_options: |_, _| Default::default(),
 665            session,
 666        })
 667    }
 668}
 669
 670struct DelayedDebouncedEditAction {
 671    task: Option<Task<()>>,
 672    cancel_channel: Option<oneshot::Sender<()>>,
 673}
 674
 675impl DelayedDebouncedEditAction {
 676    fn new() -> DelayedDebouncedEditAction {
 677        DelayedDebouncedEditAction {
 678            task: None,
 679            cancel_channel: None,
 680        }
 681    }
 682
 683    fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
 684    where
 685        F: 'static + Send + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
 686    {
 687        if let Some(channel) = self.cancel_channel.take() {
 688            _ = channel.send(());
 689        }
 690
 691        let (sender, mut receiver) = oneshot::channel::<()>();
 692        self.cancel_channel = Some(sender);
 693
 694        let previous_task = self.task.take();
 695        self.task = Some(cx.spawn(move |workspace, mut cx| async move {
 696            let mut timer = cx.background_executor().timer(delay).fuse();
 697            if let Some(previous_task) = previous_task {
 698                previous_task.await;
 699            }
 700
 701            futures::select_biased! {
 702                _ = receiver => return,
 703                    _ = timer => {}
 704            }
 705
 706            if let Some(result) = workspace
 707                .update(&mut cx, |workspace, cx| (func)(workspace, cx))
 708                .log_err()
 709            {
 710                result.await.log_err();
 711            }
 712        }));
 713    }
 714}
 715
 716pub enum Event {
 717    PaneAdded(View<Pane>),
 718    PaneRemoved,
 719    ItemAdded {
 720        item: Box<dyn ItemHandle>,
 721    },
 722    ItemRemoved,
 723    ActiveItemChanged,
 724    UserSavedItem {
 725        pane: WeakView<Pane>,
 726        item: Box<dyn WeakItemHandle>,
 727        save_intent: SaveIntent,
 728    },
 729    ContactRequestedJoin(u64),
 730    WorkspaceCreated(WeakView<Workspace>),
 731    SpawnTask {
 732        action: Box<SpawnInTerminal>,
 733    },
 734    OpenBundledFile {
 735        text: Cow<'static, str>,
 736        title: &'static str,
 737        language: &'static str,
 738    },
 739    ZoomChanged,
 740}
 741
 742#[derive(Debug)]
 743pub enum OpenVisible {
 744    All,
 745    None,
 746    OnlyFiles,
 747    OnlyDirectories,
 748}
 749
 750type PromptForNewPath = Box<
 751    dyn Fn(&mut Workspace, &mut ViewContext<Workspace>) -> oneshot::Receiver<Option<ProjectPath>>,
 752>;
 753
 754type PromptForOpenPath = Box<
 755    dyn Fn(
 756        &mut Workspace,
 757        DirectoryLister,
 758        &mut ViewContext<Workspace>,
 759    ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
 760>;
 761
 762/// Collects everything project-related for a certain window opened.
 763/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
 764///
 765/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
 766/// The `Workspace` owns everybody's state and serves as a default, "global context",
 767/// that can be used to register a global action to be triggered from any place in the window.
 768pub struct Workspace {
 769    weak_self: WeakView<Self>,
 770    workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
 771    zoomed: Option<AnyWeakView>,
 772    previous_dock_drag_coordinates: Option<Point<Pixels>>,
 773    zoomed_position: Option<DockPosition>,
 774    center: PaneGroup,
 775    left_dock: View<Dock>,
 776    bottom_dock: View<Dock>,
 777    right_dock: View<Dock>,
 778    panes: Vec<View<Pane>>,
 779    panes_by_item: HashMap<EntityId, WeakView<Pane>>,
 780    active_pane: View<Pane>,
 781    last_active_center_pane: Option<WeakView<Pane>>,
 782    last_active_view_id: Option<proto::ViewId>,
 783    status_bar: View<StatusBar>,
 784    modal_layer: View<ModalLayer>,
 785    titlebar_item: Option<AnyView>,
 786    notifications: Vec<(NotificationId, AnyView)>,
 787    project: Model<Project>,
 788    follower_states: HashMap<PeerId, FollowerState>,
 789    last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
 790    window_edited: bool,
 791    active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
 792    leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
 793    database_id: Option<WorkspaceId>,
 794    app_state: Arc<AppState>,
 795    dispatching_keystrokes: Rc<RefCell<(HashSet<String>, Vec<Keystroke>)>>,
 796    _subscriptions: Vec<Subscription>,
 797    _apply_leader_updates: Task<Result<()>>,
 798    _observe_current_user: Task<Result<()>>,
 799    _schedule_serialize: Option<Task<()>>,
 800    pane_history_timestamp: Arc<AtomicUsize>,
 801    bounds: Bounds<Pixels>,
 802    centered_layout: bool,
 803    bounds_save_task_queued: Option<Task<()>>,
 804    on_prompt_for_new_path: Option<PromptForNewPath>,
 805    on_prompt_for_open_path: Option<PromptForOpenPath>,
 806    serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
 807    serialized_ssh_project: Option<SerializedSshProject>,
 808    _items_serializer: Task<Result<()>>,
 809    session_id: Option<String>,
 810}
 811
 812impl EventEmitter<Event> for Workspace {}
 813
 814#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 815pub struct ViewId {
 816    pub creator: PeerId,
 817    pub id: u64,
 818}
 819
 820pub struct FollowerState {
 821    center_pane: View<Pane>,
 822    dock_pane: Option<View<Pane>>,
 823    active_view_id: Option<ViewId>,
 824    items_by_leader_view_id: HashMap<ViewId, FollowerView>,
 825}
 826
 827struct FollowerView {
 828    view: Box<dyn FollowableItemHandle>,
 829    location: Option<proto::PanelId>,
 830}
 831
 832impl Workspace {
 833    const DEFAULT_PADDING: f32 = 0.2;
 834    const MAX_PADDING: f32 = 0.4;
 835
 836    pub fn new(
 837        workspace_id: Option<WorkspaceId>,
 838        project: Model<Project>,
 839        app_state: Arc<AppState>,
 840        cx: &mut ViewContext<Self>,
 841    ) -> Self {
 842        cx.observe(&project, |_, _, cx| cx.notify()).detach();
 843        cx.subscribe(&project, move |this, _, event, cx| {
 844            match event {
 845                project::Event::RemoteIdChanged(_) => {
 846                    this.update_window_title(cx);
 847                }
 848
 849                project::Event::CollaboratorLeft(peer_id) => {
 850                    this.collaborator_left(*peer_id, cx);
 851                }
 852
 853                project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
 854                    this.update_window_title(cx);
 855                    this.serialize_workspace(cx);
 856                }
 857
 858                project::Event::DisconnectedFromHost => {
 859                    this.update_window_edited(cx);
 860                    let leaders_to_unfollow =
 861                        this.follower_states.keys().copied().collect::<Vec<_>>();
 862                    for leader_id in leaders_to_unfollow {
 863                        this.unfollow(leader_id, cx);
 864                    }
 865                }
 866
 867                project::Event::DisconnectedFromSshRemote => {
 868                    this.update_window_edited(cx);
 869                }
 870
 871                project::Event::Closed => {
 872                    cx.remove_window();
 873                }
 874
 875                project::Event::DeletedEntry(_, entry_id) => {
 876                    for pane in this.panes.iter() {
 877                        pane.update(cx, |pane, cx| {
 878                            pane.handle_deleted_project_item(*entry_id, cx)
 879                        });
 880                    }
 881                }
 882
 883                project::Event::Toast {
 884                    notification_id,
 885                    message,
 886                } => this.show_notification(
 887                    NotificationId::named(notification_id.clone()),
 888                    cx,
 889                    |cx| cx.new_view(|_| MessageNotification::new(message.clone())),
 890                ),
 891
 892                project::Event::HideToast { notification_id } => {
 893                    this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
 894                }
 895
 896                project::Event::LanguageServerPrompt(request) => {
 897                    struct LanguageServerPrompt;
 898
 899                    let mut hasher = DefaultHasher::new();
 900                    request.lsp_name.as_str().hash(&mut hasher);
 901                    let id = hasher.finish();
 902
 903                    this.show_notification(
 904                        NotificationId::composite::<LanguageServerPrompt>(id as usize),
 905                        cx,
 906                        |cx| {
 907                            cx.new_view(|_| {
 908                                notifications::LanguageServerPrompt::new(request.clone())
 909                            })
 910                        },
 911                    );
 912                }
 913
 914                _ => {}
 915            }
 916            cx.notify()
 917        })
 918        .detach();
 919
 920        cx.on_focus_lost(|this, cx| {
 921            let focus_handle = this.focus_handle(cx);
 922            cx.focus(&focus_handle);
 923        })
 924        .detach();
 925
 926        let weak_handle = cx.view().downgrade();
 927        let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
 928
 929        let center_pane = cx.new_view(|cx| {
 930            let mut center_pane = Pane::new(
 931                weak_handle.clone(),
 932                project.clone(),
 933                pane_history_timestamp.clone(),
 934                None,
 935                NewFile.boxed_clone(),
 936                cx,
 937            );
 938            center_pane.set_can_split(Some(Arc::new(|_, _, _| true)));
 939            center_pane
 940        });
 941        cx.subscribe(&center_pane, Self::handle_pane_event).detach();
 942
 943        cx.focus_view(&center_pane);
 944        cx.emit(Event::PaneAdded(center_pane.clone()));
 945
 946        let window_handle = cx.window_handle().downcast::<Workspace>().unwrap();
 947        app_state.workspace_store.update(cx, |store, _| {
 948            store.workspaces.insert(window_handle);
 949        });
 950
 951        let mut current_user = app_state.user_store.read(cx).watch_current_user();
 952        let mut connection_status = app_state.client.status();
 953        let _observe_current_user = cx.spawn(|this, mut cx| async move {
 954            current_user.next().await;
 955            connection_status.next().await;
 956            let mut stream =
 957                Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
 958
 959            while stream.recv().await.is_some() {
 960                this.update(&mut cx, |_, cx| cx.notify())?;
 961            }
 962            anyhow::Ok(())
 963        });
 964
 965        // All leader updates are enqueued and then processed in a single task, so
 966        // that each asynchronous operation can be run in order.
 967        let (leader_updates_tx, mut leader_updates_rx) =
 968            mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
 969        let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
 970            while let Some((leader_id, update)) = leader_updates_rx.next().await {
 971                Self::process_leader_update(&this, leader_id, update, &mut cx)
 972                    .await
 973                    .log_err();
 974            }
 975
 976            Ok(())
 977        });
 978
 979        cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
 980
 981        let left_dock = Dock::new(DockPosition::Left, cx);
 982        let bottom_dock = Dock::new(DockPosition::Bottom, cx);
 983        let right_dock = Dock::new(DockPosition::Right, cx);
 984        let left_dock_buttons = cx.new_view(|cx| PanelButtons::new(left_dock.clone(), cx));
 985        let bottom_dock_buttons = cx.new_view(|cx| PanelButtons::new(bottom_dock.clone(), cx));
 986        let right_dock_buttons = cx.new_view(|cx| PanelButtons::new(right_dock.clone(), cx));
 987        let status_bar = cx.new_view(|cx| {
 988            let mut status_bar = StatusBar::new(&center_pane.clone(), cx);
 989            status_bar.add_left_item(left_dock_buttons, cx);
 990            status_bar.add_right_item(right_dock_buttons, cx);
 991            status_bar.add_right_item(bottom_dock_buttons, cx);
 992            status_bar
 993        });
 994
 995        let modal_layer = cx.new_view(|_| ModalLayer::new());
 996
 997        let session_id = app_state.session.read(cx).id().to_owned();
 998
 999        let mut active_call = None;
1000        if let Some(call) = ActiveCall::try_global(cx) {
1001            let call = call.clone();
1002            let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
1003            active_call = Some((call, subscriptions));
1004        }
1005
1006        let (serializable_items_tx, serializable_items_rx) =
1007            mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1008        let _items_serializer = cx.spawn(|this, mut cx| async move {
1009            Self::serialize_items(&this, serializable_items_rx, &mut cx).await
1010        });
1011
1012        let subscriptions = vec![
1013            cx.observe_window_activation(Self::on_window_activation_changed),
1014            cx.observe_window_bounds(move |this, cx| {
1015                if this.bounds_save_task_queued.is_some() {
1016                    return;
1017                }
1018                this.bounds_save_task_queued = Some(cx.spawn(|this, mut cx| async move {
1019                    cx.background_executor()
1020                        .timer(Duration::from_millis(100))
1021                        .await;
1022                    this.update(&mut cx, |this, cx| {
1023                        if let Some(display) = cx.display() {
1024                            if let Ok(display_uuid) = display.uuid() {
1025                                let window_bounds = cx.inner_window_bounds();
1026                                if let Some(database_id) = workspace_id {
1027                                    cx.background_executor()
1028                                        .spawn(DB.set_window_open_status(
1029                                            database_id,
1030                                            SerializedWindowBounds(window_bounds),
1031                                            display_uuid,
1032                                        ))
1033                                        .detach_and_log_err(cx);
1034                                }
1035                            }
1036                        }
1037                        this.bounds_save_task_queued.take();
1038                    })
1039                    .ok();
1040                }));
1041                cx.notify();
1042            }),
1043            cx.observe_window_appearance(|_, cx| {
1044                let window_appearance = cx.appearance();
1045
1046                *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1047
1048                ThemeSettings::reload_current_theme(cx);
1049            }),
1050            cx.on_release(|this, window, cx| {
1051                this.app_state.workspace_store.update(cx, |store, _| {
1052                    let window = window.downcast::<Self>().unwrap();
1053                    store.workspaces.remove(&window);
1054                })
1055            }),
1056        ];
1057
1058        cx.defer(|this, cx| {
1059            this.update_window_title(cx);
1060            this.show_initial_notifications(cx);
1061        });
1062        Workspace {
1063            weak_self: weak_handle.clone(),
1064            zoomed: None,
1065            zoomed_position: None,
1066            previous_dock_drag_coordinates: None,
1067            center: PaneGroup::new(center_pane.clone()),
1068            panes: vec![center_pane.clone()],
1069            panes_by_item: Default::default(),
1070            active_pane: center_pane.clone(),
1071            last_active_center_pane: Some(center_pane.downgrade()),
1072            last_active_view_id: None,
1073            status_bar,
1074            modal_layer,
1075            titlebar_item: None,
1076            notifications: Default::default(),
1077            left_dock,
1078            bottom_dock,
1079            right_dock,
1080            project: project.clone(),
1081            follower_states: Default::default(),
1082            last_leaders_by_pane: Default::default(),
1083            dispatching_keystrokes: Default::default(),
1084            window_edited: false,
1085            active_call,
1086            database_id: workspace_id,
1087            app_state,
1088            _observe_current_user,
1089            _apply_leader_updates,
1090            _schedule_serialize: None,
1091            leader_updates_tx,
1092            _subscriptions: subscriptions,
1093            pane_history_timestamp,
1094            workspace_actions: Default::default(),
1095            // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1096            bounds: Default::default(),
1097            centered_layout: false,
1098            bounds_save_task_queued: None,
1099            on_prompt_for_new_path: None,
1100            on_prompt_for_open_path: None,
1101            serializable_items_tx,
1102            _items_serializer,
1103            session_id: Some(session_id),
1104            serialized_ssh_project: None,
1105        }
1106    }
1107
1108    pub fn new_local(
1109        abs_paths: Vec<PathBuf>,
1110        app_state: Arc<AppState>,
1111        requesting_window: Option<WindowHandle<Workspace>>,
1112        env: Option<HashMap<String, String>>,
1113        cx: &mut AppContext,
1114    ) -> Task<
1115        anyhow::Result<(
1116            WindowHandle<Workspace>,
1117            Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
1118        )>,
1119    > {
1120        let project_handle = Project::local(
1121            app_state.client.clone(),
1122            app_state.node_runtime.clone(),
1123            app_state.user_store.clone(),
1124            app_state.languages.clone(),
1125            app_state.fs.clone(),
1126            env,
1127            cx,
1128        );
1129
1130        cx.spawn(|mut cx| async move {
1131            let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1132            for path in abs_paths.into_iter() {
1133                if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1134                    paths_to_open.push(canonical)
1135                } else {
1136                    paths_to_open.push(path)
1137                }
1138            }
1139
1140            let serialized_workspace: Option<SerializedWorkspace> =
1141                persistence::DB.workspace_for_roots(paths_to_open.as_slice());
1142
1143            let workspace_location = serialized_workspace
1144                .as_ref()
1145                .map(|ws| &ws.location)
1146                .and_then(|loc| match loc {
1147                    SerializedWorkspaceLocation::Local(_, order) => {
1148                        Some((loc.sorted_paths(), order.order()))
1149                    }
1150                    _ => None,
1151                });
1152
1153            if let Some((paths, order)) = workspace_location {
1154                paths_to_open = paths.iter().cloned().collect();
1155
1156                if order.iter().enumerate().any(|(i, &j)| i != j) {
1157                    project_handle
1158                        .update(&mut cx, |project, cx| {
1159                            project.set_worktrees_reordered(true, cx);
1160                        })
1161                        .log_err();
1162                }
1163            }
1164
1165            // Get project paths for all of the abs_paths
1166            let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1167                Vec::with_capacity(paths_to_open.len());
1168            for path in paths_to_open.into_iter() {
1169                if let Some((_, project_entry)) = cx
1170                    .update(|cx| {
1171                        Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1172                    })?
1173                    .await
1174                    .log_err()
1175                {
1176                    project_paths.push((path, Some(project_entry)));
1177                } else {
1178                    project_paths.push((path, None));
1179                }
1180            }
1181
1182            let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1183                serialized_workspace.id
1184            } else {
1185                DB.next_id().await.unwrap_or_else(|_| Default::default())
1186            };
1187
1188            let toolchains = DB.toolchains(workspace_id).await?;
1189            for (toolchain, worktree_id) in toolchains {
1190                project_handle
1191                    .update(&mut cx, |this, cx| {
1192                        this.activate_toolchain(worktree_id, toolchain, cx)
1193                    })?
1194                    .await;
1195            }
1196            let window = if let Some(window) = requesting_window {
1197                cx.update_window(window.into(), |_, cx| {
1198                    cx.replace_root_view(|cx| {
1199                        Workspace::new(
1200                            Some(workspace_id),
1201                            project_handle.clone(),
1202                            app_state.clone(),
1203                            cx,
1204                        )
1205                    });
1206                })?;
1207                window
1208            } else {
1209                let window_bounds_override = window_bounds_env_override();
1210
1211                let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1212                    (Some(WindowBounds::Windowed(bounds)), None)
1213                } else {
1214                    let restorable_bounds = serialized_workspace
1215                        .as_ref()
1216                        .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
1217                        .or_else(|| {
1218                            let (display, window_bounds) = DB.last_window().log_err()?;
1219                            Some((display?, window_bounds?))
1220                        });
1221
1222                    if let Some((serialized_display, serialized_status)) = restorable_bounds {
1223                        (Some(serialized_status.0), Some(serialized_display))
1224                    } else {
1225                        (None, None)
1226                    }
1227                };
1228
1229                // Use the serialized workspace to construct the new window
1230                let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?;
1231                options.window_bounds = window_bounds;
1232                let centered_layout = serialized_workspace
1233                    .as_ref()
1234                    .map(|w| w.centered_layout)
1235                    .unwrap_or(false);
1236                cx.open_window(options, {
1237                    let app_state = app_state.clone();
1238                    let project_handle = project_handle.clone();
1239                    move |cx| {
1240                        cx.new_view(|cx| {
1241                            let mut workspace =
1242                                Workspace::new(Some(workspace_id), project_handle, app_state, cx);
1243                            workspace.centered_layout = centered_layout;
1244                            workspace
1245                        })
1246                    }
1247                })?
1248            };
1249
1250            notify_if_database_failed(window, &mut cx);
1251            let opened_items = window
1252                .update(&mut cx, |_workspace, cx| {
1253                    open_items(serialized_workspace, project_paths, cx)
1254                })?
1255                .await
1256                .unwrap_or_default();
1257
1258            window
1259                .update(&mut cx, |_, cx| {
1260                    cx.activate_window();
1261                    cx.activate(true);
1262                })
1263                .log_err();
1264            Ok((window, opened_items))
1265        })
1266    }
1267
1268    pub fn weak_handle(&self) -> WeakView<Self> {
1269        self.weak_self.clone()
1270    }
1271
1272    pub fn left_dock(&self) -> &View<Dock> {
1273        &self.left_dock
1274    }
1275
1276    pub fn bottom_dock(&self) -> &View<Dock> {
1277        &self.bottom_dock
1278    }
1279
1280    pub fn right_dock(&self) -> &View<Dock> {
1281        &self.right_dock
1282    }
1283
1284    pub fn is_edited(&self) -> bool {
1285        self.window_edited
1286    }
1287
1288    pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
1289        let focus_handle = panel.focus_handle(cx);
1290        cx.on_focus_in(&focus_handle, Self::handle_panel_focused)
1291            .detach();
1292
1293        let dock = match panel.position(cx) {
1294            DockPosition::Left => &self.left_dock,
1295            DockPosition::Bottom => &self.bottom_dock,
1296            DockPosition::Right => &self.right_dock,
1297        };
1298
1299        dock.update(cx, |dock, cx| {
1300            dock.add_panel(panel, self.weak_self.clone(), cx)
1301        });
1302    }
1303
1304    pub fn status_bar(&self) -> &View<StatusBar> {
1305        &self.status_bar
1306    }
1307
1308    pub fn app_state(&self) -> &Arc<AppState> {
1309        &self.app_state
1310    }
1311
1312    pub fn user_store(&self) -> &Model<UserStore> {
1313        &self.app_state.user_store
1314    }
1315
1316    pub fn project(&self) -> &Model<Project> {
1317        &self.project
1318    }
1319
1320    pub fn recent_navigation_history_iter(
1321        &self,
1322        cx: &AppContext,
1323    ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> {
1324        let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
1325        let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
1326        for pane in &self.panes {
1327            let pane = pane.read(cx);
1328            pane.nav_history()
1329                .for_each_entry(cx, |entry, (project_path, fs_path)| {
1330                    if let Some(fs_path) = &fs_path {
1331                        abs_paths_opened
1332                            .entry(fs_path.clone())
1333                            .or_default()
1334                            .insert(project_path.clone());
1335                    }
1336                    let timestamp = entry.timestamp;
1337                    match history.entry(project_path) {
1338                        hash_map::Entry::Occupied(mut entry) => {
1339                            let (_, old_timestamp) = entry.get();
1340                            if &timestamp > old_timestamp {
1341                                entry.insert((fs_path, timestamp));
1342                            }
1343                        }
1344                        hash_map::Entry::Vacant(entry) => {
1345                            entry.insert((fs_path, timestamp));
1346                        }
1347                    }
1348                });
1349        }
1350
1351        history
1352            .into_iter()
1353            .sorted_by_key(|(_, (_, timestamp))| *timestamp)
1354            .map(|(project_path, (fs_path, _))| (project_path, fs_path))
1355            .rev()
1356            .filter(move |(history_path, abs_path)| {
1357                let latest_project_path_opened = abs_path
1358                    .as_ref()
1359                    .and_then(|abs_path| abs_paths_opened.get(abs_path))
1360                    .and_then(|project_paths| {
1361                        project_paths
1362                            .iter()
1363                            .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
1364                    });
1365
1366                match latest_project_path_opened {
1367                    Some(latest_project_path_opened) => latest_project_path_opened == history_path,
1368                    None => true,
1369                }
1370            })
1371    }
1372
1373    pub fn recent_navigation_history(
1374        &self,
1375        limit: Option<usize>,
1376        cx: &AppContext,
1377    ) -> Vec<(ProjectPath, Option<PathBuf>)> {
1378        self.recent_navigation_history_iter(cx)
1379            .take(limit.unwrap_or(usize::MAX))
1380            .collect()
1381    }
1382
1383    fn navigate_history(
1384        &mut self,
1385        pane: WeakView<Pane>,
1386        mode: NavigationMode,
1387        cx: &mut ViewContext<Workspace>,
1388    ) -> Task<Result<()>> {
1389        let to_load = if let Some(pane) = pane.upgrade() {
1390            pane.update(cx, |pane, cx| {
1391                pane.focus(cx);
1392                loop {
1393                    // Retrieve the weak item handle from the history.
1394                    let entry = pane.nav_history_mut().pop(mode, cx)?;
1395
1396                    // If the item is still present in this pane, then activate it.
1397                    if let Some(index) = entry
1398                        .item
1399                        .upgrade()
1400                        .and_then(|v| pane.index_for_item(v.as_ref()))
1401                    {
1402                        let prev_active_item_index = pane.active_item_index();
1403                        pane.nav_history_mut().set_mode(mode);
1404                        pane.activate_item(index, true, true, cx);
1405                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
1406
1407                        let mut navigated = prev_active_item_index != pane.active_item_index();
1408                        if let Some(data) = entry.data {
1409                            navigated |= pane.active_item()?.navigate(data, cx);
1410                        }
1411
1412                        if navigated {
1413                            break None;
1414                        }
1415                    } else {
1416                        // If the item is no longer present in this pane, then retrieve its
1417                        // path info in order to reopen it.
1418                        break pane
1419                            .nav_history()
1420                            .path_for_item(entry.item.id())
1421                            .map(|(project_path, abs_path)| (project_path, abs_path, entry));
1422                    }
1423                }
1424            })
1425        } else {
1426            None
1427        };
1428
1429        if let Some((project_path, abs_path, entry)) = to_load {
1430            // If the item was no longer present, then load it again from its previous path, first try the local path
1431            let open_by_project_path = self.load_path(project_path.clone(), cx);
1432
1433            cx.spawn(|workspace, mut cx| async move {
1434                let open_by_project_path = open_by_project_path.await;
1435                let mut navigated = false;
1436                match open_by_project_path
1437                    .with_context(|| format!("Navigating to {project_path:?}"))
1438                {
1439                    Ok((project_entry_id, build_item)) => {
1440                        let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1441                            pane.nav_history_mut().set_mode(mode);
1442                            pane.active_item().map(|p| p.item_id())
1443                        })?;
1444
1445                        pane.update(&mut cx, |pane, cx| {
1446                            let item = pane.open_item(
1447                                project_entry_id,
1448                                true,
1449                                entry.is_preview,
1450                                None,
1451                                cx,
1452                                build_item,
1453                            );
1454                            navigated |= Some(item.item_id()) != prev_active_item_id;
1455                            pane.nav_history_mut().set_mode(NavigationMode::Normal);
1456                            if let Some(data) = entry.data {
1457                                navigated |= item.navigate(data, cx);
1458                            }
1459                        })?;
1460                    }
1461                    Err(open_by_project_path_e) => {
1462                        // Fall back to opening by abs path, in case an external file was opened and closed,
1463                        // and its worktree is now dropped
1464                        if let Some(abs_path) = abs_path {
1465                            let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1466                                pane.nav_history_mut().set_mode(mode);
1467                                pane.active_item().map(|p| p.item_id())
1468                            })?;
1469                            let open_by_abs_path = workspace.update(&mut cx, |workspace, cx| {
1470                                workspace.open_abs_path(abs_path.clone(), false, cx)
1471                            })?;
1472                            match open_by_abs_path
1473                                .await
1474                                .with_context(|| format!("Navigating to {abs_path:?}"))
1475                            {
1476                                Ok(item) => {
1477                                    pane.update(&mut cx, |pane, cx| {
1478                                        navigated |= Some(item.item_id()) != prev_active_item_id;
1479                                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
1480                                        if let Some(data) = entry.data {
1481                                            navigated |= item.navigate(data, cx);
1482                                        }
1483                                    })?;
1484                                }
1485                                Err(open_by_abs_path_e) => {
1486                                    log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
1487                                }
1488                            }
1489                        }
1490                    }
1491                }
1492
1493                if !navigated {
1494                    workspace
1495                        .update(&mut cx, |workspace, cx| {
1496                            Self::navigate_history(workspace, pane, mode, cx)
1497                        })?
1498                        .await?;
1499                }
1500
1501                Ok(())
1502            })
1503        } else {
1504            Task::ready(Ok(()))
1505        }
1506    }
1507
1508    pub fn go_back(
1509        &mut self,
1510        pane: WeakView<Pane>,
1511        cx: &mut ViewContext<Workspace>,
1512    ) -> Task<Result<()>> {
1513        self.navigate_history(pane, NavigationMode::GoingBack, cx)
1514    }
1515
1516    pub fn go_forward(
1517        &mut self,
1518        pane: WeakView<Pane>,
1519        cx: &mut ViewContext<Workspace>,
1520    ) -> Task<Result<()>> {
1521        self.navigate_history(pane, NavigationMode::GoingForward, cx)
1522    }
1523
1524    pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
1525        self.navigate_history(
1526            self.active_pane().downgrade(),
1527            NavigationMode::ReopeningClosedItem,
1528            cx,
1529        )
1530    }
1531
1532    pub fn client(&self) -> &Arc<Client> {
1533        &self.app_state.client
1534    }
1535
1536    pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
1537        self.titlebar_item = Some(item);
1538        cx.notify();
1539    }
1540
1541    pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
1542        self.on_prompt_for_new_path = Some(prompt)
1543    }
1544
1545    pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
1546        self.on_prompt_for_open_path = Some(prompt)
1547    }
1548
1549    pub fn serialized_ssh_project(&self) -> Option<SerializedSshProject> {
1550        self.serialized_ssh_project.clone()
1551    }
1552
1553    pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) {
1554        self.serialized_ssh_project = Some(serialized_ssh_project);
1555    }
1556
1557    pub fn prompt_for_open_path(
1558        &mut self,
1559        path_prompt_options: PathPromptOptions,
1560        lister: DirectoryLister,
1561        cx: &mut ViewContext<Self>,
1562    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
1563        if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
1564            let prompt = self.on_prompt_for_open_path.take().unwrap();
1565            let rx = prompt(self, lister, cx);
1566            self.on_prompt_for_open_path = Some(prompt);
1567            rx
1568        } else {
1569            let (tx, rx) = oneshot::channel();
1570            let abs_path = cx.prompt_for_paths(path_prompt_options);
1571
1572            cx.spawn(|this, mut cx| async move {
1573                let Ok(result) = abs_path.await else {
1574                    return Ok(());
1575                };
1576
1577                match result {
1578                    Ok(result) => {
1579                        tx.send(result).log_err();
1580                    }
1581                    Err(err) => {
1582                        let rx = this.update(&mut cx, |this, cx| {
1583                            this.show_portal_error(err.to_string(), cx);
1584                            let prompt = this.on_prompt_for_open_path.take().unwrap();
1585                            let rx = prompt(this, lister, cx);
1586                            this.on_prompt_for_open_path = Some(prompt);
1587                            rx
1588                        })?;
1589                        if let Ok(path) = rx.await {
1590                            tx.send(path).log_err();
1591                        }
1592                    }
1593                };
1594                anyhow::Ok(())
1595            })
1596            .detach();
1597
1598            rx
1599        }
1600    }
1601
1602    pub fn prompt_for_new_path(
1603        &mut self,
1604        cx: &mut ViewContext<Self>,
1605    ) -> oneshot::Receiver<Option<ProjectPath>> {
1606        if (self.project.read(cx).is_via_collab() || self.project.read(cx).is_via_ssh())
1607            || !WorkspaceSettings::get_global(cx).use_system_path_prompts
1608        {
1609            let prompt = self.on_prompt_for_new_path.take().unwrap();
1610            let rx = prompt(self, cx);
1611            self.on_prompt_for_new_path = Some(prompt);
1612            rx
1613        } else {
1614            let start_abs_path = self
1615                .project
1616                .update(cx, |project, cx| {
1617                    let worktree = project.visible_worktrees(cx).next()?;
1618                    Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
1619                })
1620                .unwrap_or_else(|| Path::new("").into());
1621
1622            let (tx, rx) = oneshot::channel();
1623            let abs_path = cx.prompt_for_new_path(&start_abs_path);
1624            cx.spawn(|this, mut cx| async move {
1625                let abs_path = match abs_path.await? {
1626                    Ok(path) => path,
1627                    Err(err) => {
1628                        let rx = this.update(&mut cx, |this, cx| {
1629                            this.show_portal_error(err.to_string(), cx);
1630
1631                            let prompt = this.on_prompt_for_new_path.take().unwrap();
1632                            let rx = prompt(this, cx);
1633                            this.on_prompt_for_new_path = Some(prompt);
1634                            rx
1635                        })?;
1636                        if let Ok(path) = rx.await {
1637                            tx.send(path).log_err();
1638                        }
1639                        return anyhow::Ok(());
1640                    }
1641                };
1642
1643                let project_path = abs_path.and_then(|abs_path| {
1644                    this.update(&mut cx, |this, cx| {
1645                        this.project.update(cx, |project, cx| {
1646                            project.find_or_create_worktree(abs_path, true, cx)
1647                        })
1648                    })
1649                    .ok()
1650                });
1651
1652                if let Some(project_path) = project_path {
1653                    let (worktree, path) = project_path.await?;
1654                    let worktree_id = worktree.read_with(&cx, |worktree, _| worktree.id())?;
1655                    tx.send(Some(ProjectPath {
1656                        worktree_id,
1657                        path: path.into(),
1658                    }))
1659                    .ok();
1660                } else {
1661                    tx.send(None).ok();
1662                }
1663                anyhow::Ok(())
1664            })
1665            .detach_and_log_err(cx);
1666
1667            rx
1668        }
1669    }
1670
1671    pub fn titlebar_item(&self) -> Option<AnyView> {
1672        self.titlebar_item.clone()
1673    }
1674
1675    /// Call the given callback with a workspace whose project is local.
1676    ///
1677    /// If the given workspace has a local project, then it will be passed
1678    /// to the callback. Otherwise, a new empty window will be created.
1679    pub fn with_local_workspace<T, F>(
1680        &mut self,
1681        cx: &mut ViewContext<Self>,
1682        callback: F,
1683    ) -> Task<Result<T>>
1684    where
1685        T: 'static,
1686        F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1687    {
1688        if self.project.read(cx).is_local() {
1689            Task::ready(Ok(callback(self, cx)))
1690        } else {
1691            let env = self.project.read(cx).cli_environment(cx);
1692            let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx);
1693            cx.spawn(|_vh, mut cx| async move {
1694                let (workspace, _) = task.await?;
1695                workspace.update(&mut cx, callback)
1696            })
1697        }
1698    }
1699
1700    pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Model<Worktree>> {
1701        self.project.read(cx).worktrees(cx)
1702    }
1703
1704    pub fn visible_worktrees<'a>(
1705        &self,
1706        cx: &'a AppContext,
1707    ) -> impl 'a + Iterator<Item = Model<Worktree>> {
1708        self.project.read(cx).visible_worktrees(cx)
1709    }
1710
1711    pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1712        let futures = self
1713            .worktrees(cx)
1714            .filter_map(|worktree| worktree.read(cx).as_local())
1715            .map(|worktree| worktree.scan_complete())
1716            .collect::<Vec<_>>();
1717        async move {
1718            for future in futures {
1719                future.await;
1720            }
1721        }
1722    }
1723
1724    pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1725        cx.defer(|cx| {
1726            cx.windows().iter().find(|window| {
1727                window
1728                    .update(cx, |_, cx| {
1729                        if cx.is_window_active() {
1730                            //This can only get called when the window's project connection has been lost
1731                            //so we don't need to prompt the user for anything and instead just close the window
1732                            cx.remove_window();
1733                            true
1734                        } else {
1735                            false
1736                        }
1737                    })
1738                    .unwrap_or(false)
1739            });
1740        });
1741    }
1742
1743    pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext<Self>) {
1744        let prepare = self.prepare_to_close(CloseIntent::CloseWindow, cx);
1745        let window = cx.window_handle();
1746        cx.spawn(|_, mut cx| async move {
1747            if prepare.await? {
1748                window.update(&mut cx, |_, cx| {
1749                    cx.remove_window();
1750                })?;
1751            }
1752            anyhow::Ok(())
1753        })
1754        .detach_and_log_err(cx)
1755    }
1756
1757    pub fn move_focused_panel_to_next_position(
1758        &mut self,
1759        _: &MoveFocusedPanelToNextPosition,
1760        cx: &mut ViewContext<Self>,
1761    ) {
1762        let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1763        let active_dock = docks
1764            .into_iter()
1765            .find(|dock| dock.focus_handle(cx).contains_focused(cx));
1766
1767        if let Some(dock) = active_dock {
1768            dock.update(cx, |dock, cx| {
1769                let active_panel = dock
1770                    .active_panel()
1771                    .filter(|panel| panel.focus_handle(cx).contains_focused(cx));
1772
1773                if let Some(panel) = active_panel {
1774                    panel.move_to_next_position(cx);
1775                }
1776            })
1777        }
1778    }
1779
1780    pub fn prepare_to_close(
1781        &mut self,
1782        close_intent: CloseIntent,
1783        cx: &mut ViewContext<Self>,
1784    ) -> Task<Result<bool>> {
1785        let active_call = self.active_call().cloned();
1786        let window = cx.window_handle();
1787
1788        // On Linux and Windows, closing the last window should restore the last workspace.
1789        let save_last_workspace = cfg!(not(target_os = "macos"))
1790            && close_intent != CloseIntent::ReplaceWindow
1791            && cx.windows().len() == 1;
1792
1793        cx.spawn(|this, mut cx| async move {
1794            let workspace_count = (*cx).update(|cx| {
1795                cx.windows()
1796                    .iter()
1797                    .filter(|window| window.downcast::<Workspace>().is_some())
1798                    .count()
1799            })?;
1800
1801            if let Some(active_call) = active_call {
1802                if close_intent != CloseIntent::Quit
1803                    && workspace_count == 1
1804                    && active_call.read_with(&cx, |call, _| call.room().is_some())?
1805                {
1806                    let answer = window.update(&mut cx, |_, cx| {
1807                        cx.prompt(
1808                            PromptLevel::Warning,
1809                            "Do you want to leave the current call?",
1810                            None,
1811                            &["Close window and hang up", "Cancel"],
1812                        )
1813                    })?;
1814
1815                    if answer.await.log_err() == Some(1) {
1816                        return anyhow::Ok(false);
1817                    } else {
1818                        active_call
1819                            .update(&mut cx, |call, cx| call.hang_up(cx))?
1820                            .await
1821                            .log_err();
1822                    }
1823                }
1824            }
1825
1826            let save_result = this
1827                .update(&mut cx, |this, cx| {
1828                    this.save_all_internal(SaveIntent::Close, cx)
1829                })?
1830                .await;
1831
1832            // If we're not quitting, but closing, we remove the workspace from
1833            // the current session.
1834            if close_intent != CloseIntent::Quit
1835                && !save_last_workspace
1836                && save_result.as_ref().map_or(false, |&res| res)
1837            {
1838                this.update(&mut cx, |this, cx| this.remove_from_session(cx))?
1839                    .await;
1840            }
1841
1842            save_result
1843        })
1844    }
1845
1846    fn save_all(&mut self, action: &SaveAll, cx: &mut ViewContext<Self>) {
1847        self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx)
1848            .detach_and_log_err(cx);
1849    }
1850
1851    fn send_keystrokes(&mut self, action: &SendKeystrokes, cx: &mut ViewContext<Self>) {
1852        let mut state = self.dispatching_keystrokes.borrow_mut();
1853        if !state.0.insert(action.0.clone()) {
1854            cx.propagate();
1855            return;
1856        }
1857        let mut keystrokes: Vec<Keystroke> = action
1858            .0
1859            .split(' ')
1860            .flat_map(|k| Keystroke::parse(k).log_err())
1861            .collect();
1862        keystrokes.reverse();
1863
1864        state.1.append(&mut keystrokes);
1865        drop(state);
1866
1867        let keystrokes = self.dispatching_keystrokes.clone();
1868        cx.window_context()
1869            .spawn(|mut cx| async move {
1870                // limit to 100 keystrokes to avoid infinite recursion.
1871                for _ in 0..100 {
1872                    let Some(keystroke) = keystrokes.borrow_mut().1.pop() else {
1873                        keystrokes.borrow_mut().0.clear();
1874                        return Ok(());
1875                    };
1876                    cx.update(|cx| {
1877                        let focused = cx.focused();
1878                        cx.dispatch_keystroke(keystroke.clone());
1879                        if cx.focused() != focused {
1880                            // dispatch_keystroke may cause the focus to change.
1881                            // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
1882                            // And we need that to happen before the next keystroke to keep vim mode happy...
1883                            // (Note that the tests always do this implicitly, so you must manually test with something like:
1884                            //   "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
1885                            // )
1886                            cx.draw();
1887                        }
1888                    })?;
1889                }
1890
1891                *keystrokes.borrow_mut() = Default::default();
1892                Err(anyhow!("over 100 keystrokes passed to send_keystrokes"))
1893            })
1894            .detach_and_log_err(cx);
1895    }
1896
1897    fn save_all_internal(
1898        &mut self,
1899        mut save_intent: SaveIntent,
1900        cx: &mut ViewContext<Self>,
1901    ) -> Task<Result<bool>> {
1902        if self.project.read(cx).is_disconnected(cx) {
1903            return Task::ready(Ok(true));
1904        }
1905        let dirty_items = self
1906            .panes
1907            .iter()
1908            .flat_map(|pane| {
1909                pane.read(cx).items().filter_map(|item| {
1910                    if item.is_dirty(cx) {
1911                        item.tab_description(0, cx);
1912                        Some((pane.downgrade(), item.boxed_clone()))
1913                    } else {
1914                        None
1915                    }
1916                })
1917            })
1918            .collect::<Vec<_>>();
1919
1920        let project = self.project.clone();
1921        cx.spawn(|workspace, mut cx| async move {
1922            let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
1923                let (serialize_tasks, remaining_dirty_items) =
1924                    workspace.update(&mut cx, |workspace, cx| {
1925                        let mut remaining_dirty_items = Vec::new();
1926                        let mut serialize_tasks = Vec::new();
1927                        for (pane, item) in dirty_items {
1928                            if let Some(task) = item
1929                                .to_serializable_item_handle(cx)
1930                                .and_then(|handle| handle.serialize(workspace, true, cx))
1931                            {
1932                                serialize_tasks.push(task);
1933                            } else {
1934                                remaining_dirty_items.push((pane, item));
1935                            }
1936                        }
1937                        (serialize_tasks, remaining_dirty_items)
1938                    })?;
1939
1940                futures::future::try_join_all(serialize_tasks).await?;
1941
1942                if remaining_dirty_items.len() > 1 {
1943                    let answer = workspace.update(&mut cx, |_, cx| {
1944                        let (prompt, detail) = Pane::file_names_for_prompt(
1945                            &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
1946                            remaining_dirty_items.len(),
1947                            cx,
1948                        );
1949                        cx.prompt(
1950                            PromptLevel::Warning,
1951                            &prompt,
1952                            Some(&detail),
1953                            &["Save all", "Discard all", "Cancel"],
1954                        )
1955                    })?;
1956                    match answer.await.log_err() {
1957                        Some(0) => save_intent = SaveIntent::SaveAll,
1958                        Some(1) => save_intent = SaveIntent::Skip,
1959                        _ => {}
1960                    }
1961                }
1962
1963                remaining_dirty_items
1964            } else {
1965                dirty_items
1966            };
1967
1968            for (pane, item) in dirty_items {
1969                let (singleton, project_entry_ids) =
1970                    cx.update(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
1971                if singleton || !project_entry_ids.is_empty() {
1972                    if let Some(ix) =
1973                        pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))?
1974                    {
1975                        if !Pane::save_item(
1976                            project.clone(),
1977                            &pane,
1978                            ix,
1979                            &*item,
1980                            save_intent,
1981                            &mut cx,
1982                        )
1983                        .await?
1984                        {
1985                            return Ok(false);
1986                        }
1987                    }
1988                }
1989            }
1990            Ok(true)
1991        })
1992    }
1993
1994    pub fn open_workspace_for_paths(
1995        &mut self,
1996        replace_current_window: bool,
1997        paths: Vec<PathBuf>,
1998        cx: &mut ViewContext<Self>,
1999    ) -> Task<Result<()>> {
2000        let window = cx.window_handle().downcast::<Self>();
2001        let is_remote = self.project.read(cx).is_via_collab();
2002        let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
2003        let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
2004
2005        let window_to_replace = if replace_current_window {
2006            window
2007        } else if is_remote || has_worktree || has_dirty_items {
2008            None
2009        } else {
2010            window
2011        };
2012        let app_state = self.app_state.clone();
2013
2014        cx.spawn(|_, mut cx| async move {
2015            cx.update(|cx| {
2016                open_paths(
2017                    &paths,
2018                    app_state,
2019                    OpenOptions {
2020                        replace_window: window_to_replace,
2021                        ..Default::default()
2022                    },
2023                    cx,
2024                )
2025            })?
2026            .await?;
2027            Ok(())
2028        })
2029    }
2030
2031    #[allow(clippy::type_complexity)]
2032    pub fn open_paths(
2033        &mut self,
2034        mut abs_paths: Vec<PathBuf>,
2035        visible: OpenVisible,
2036        pane: Option<WeakView<Pane>>,
2037        cx: &mut ViewContext<Self>,
2038    ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
2039        log::info!("open paths {abs_paths:?}");
2040
2041        let fs = self.app_state.fs.clone();
2042
2043        // Sort the paths to ensure we add worktrees for parents before their children.
2044        abs_paths.sort_unstable();
2045        cx.spawn(move |this, mut cx| async move {
2046            let mut tasks = Vec::with_capacity(abs_paths.len());
2047
2048            for abs_path in &abs_paths {
2049                let visible = match visible {
2050                    OpenVisible::All => Some(true),
2051                    OpenVisible::None => Some(false),
2052                    OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
2053                        Some(Some(metadata)) => Some(!metadata.is_dir),
2054                        Some(None) => Some(true),
2055                        None => None,
2056                    },
2057                    OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
2058                        Some(Some(metadata)) => Some(metadata.is_dir),
2059                        Some(None) => Some(false),
2060                        None => None,
2061                    },
2062                };
2063                let project_path = match visible {
2064                    Some(visible) => match this
2065                        .update(&mut cx, |this, cx| {
2066                            Workspace::project_path_for_path(
2067                                this.project.clone(),
2068                                abs_path,
2069                                visible,
2070                                cx,
2071                            )
2072                        })
2073                        .log_err()
2074                    {
2075                        Some(project_path) => project_path.await.log_err(),
2076                        None => None,
2077                    },
2078                    None => None,
2079                };
2080
2081                let this = this.clone();
2082                let abs_path: Arc<Path> = SanitizedPath::from(abs_path.clone()).into();
2083                let fs = fs.clone();
2084                let pane = pane.clone();
2085                let task = cx.spawn(move |mut cx| async move {
2086                    let (worktree, project_path) = project_path?;
2087                    if fs.is_dir(&abs_path).await {
2088                        this.update(&mut cx, |workspace, cx| {
2089                            let worktree = worktree.read(cx);
2090                            let worktree_abs_path = worktree.abs_path();
2091                            let entry_id = if abs_path.as_ref() == worktree_abs_path.as_ref() {
2092                                worktree.root_entry()
2093                            } else {
2094                                abs_path
2095                                    .strip_prefix(worktree_abs_path.as_ref())
2096                                    .ok()
2097                                    .and_then(|relative_path| {
2098                                        worktree.entry_for_path(relative_path)
2099                                    })
2100                            }
2101                            .map(|entry| entry.id);
2102                            if let Some(entry_id) = entry_id {
2103                                workspace.project.update(cx, |_, cx| {
2104                                    cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
2105                                })
2106                            }
2107                        })
2108                        .log_err()?;
2109                        None
2110                    } else {
2111                        Some(
2112                            this.update(&mut cx, |this, cx| {
2113                                this.open_path(project_path, pane, true, cx)
2114                            })
2115                            .log_err()?
2116                            .await,
2117                        )
2118                    }
2119                });
2120                tasks.push(task);
2121            }
2122
2123            futures::future::join_all(tasks).await
2124        })
2125    }
2126
2127    pub fn open_resolved_path(
2128        &mut self,
2129        path: ResolvedPath,
2130        cx: &mut ViewContext<Self>,
2131    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2132        match path {
2133            ResolvedPath::ProjectPath { project_path, .. } => {
2134                self.open_path(project_path, None, true, cx)
2135            }
2136            ResolvedPath::AbsPath { path, .. } => self.open_abs_path(path, false, cx),
2137        }
2138    }
2139
2140    fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
2141        let project = self.project.read(cx);
2142        if project.is_via_collab() {
2143            self.show_error(
2144                &anyhow!("You cannot add folders to someone else's project"),
2145                cx,
2146            );
2147            return;
2148        }
2149        let paths = self.prompt_for_open_path(
2150            PathPromptOptions {
2151                files: false,
2152                directories: true,
2153                multiple: true,
2154            },
2155            DirectoryLister::Project(self.project.clone()),
2156            cx,
2157        );
2158        cx.spawn(|this, mut cx| async move {
2159            if let Some(paths) = paths.await.log_err().flatten() {
2160                let results = this
2161                    .update(&mut cx, |this, cx| {
2162                        this.open_paths(paths, OpenVisible::All, None, cx)
2163                    })?
2164                    .await;
2165                for result in results.into_iter().flatten() {
2166                    result.log_err();
2167                }
2168            }
2169            anyhow::Ok(())
2170        })
2171        .detach_and_log_err(cx);
2172    }
2173
2174    pub fn project_path_for_path(
2175        project: Model<Project>,
2176        abs_path: &Path,
2177        visible: bool,
2178        cx: &mut AppContext,
2179    ) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
2180        let entry = project.update(cx, |project, cx| {
2181            project.find_or_create_worktree(abs_path, visible, cx)
2182        });
2183        cx.spawn(|mut cx| async move {
2184            let (worktree, path) = entry.await?;
2185            let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
2186            Ok((
2187                worktree,
2188                ProjectPath {
2189                    worktree_id,
2190                    path: path.into(),
2191                },
2192            ))
2193        })
2194    }
2195
2196    pub fn items<'a>(
2197        &'a self,
2198        cx: &'a AppContext,
2199    ) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
2200        self.panes.iter().flat_map(|pane| pane.read(cx).items())
2201    }
2202
2203    pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
2204        self.items_of_type(cx).max_by_key(|item| item.item_id())
2205    }
2206
2207    pub fn items_of_type<'a, T: Item>(
2208        &'a self,
2209        cx: &'a AppContext,
2210    ) -> impl 'a + Iterator<Item = View<T>> {
2211        self.panes
2212            .iter()
2213            .flat_map(|pane| pane.read(cx).items_of_type())
2214    }
2215
2216    pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
2217        self.active_pane().read(cx).active_item()
2218    }
2219
2220    pub fn active_item_as<I: 'static>(&self, cx: &AppContext) -> Option<View<I>> {
2221        let item = self.active_item(cx)?;
2222        item.to_any().downcast::<I>().ok()
2223    }
2224
2225    fn active_project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
2226        self.active_item(cx).and_then(|item| item.project_path(cx))
2227    }
2228
2229    pub fn save_active_item(
2230        &mut self,
2231        save_intent: SaveIntent,
2232        cx: &mut WindowContext,
2233    ) -> Task<Result<()>> {
2234        let project = self.project.clone();
2235        let pane = self.active_pane();
2236        let item_ix = pane.read(cx).active_item_index();
2237        let item = pane.read(cx).active_item();
2238        let pane = pane.downgrade();
2239
2240        cx.spawn(|mut cx| async move {
2241            if let Some(item) = item {
2242                Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
2243                    .await
2244                    .map(|_| ())
2245            } else {
2246                Ok(())
2247            }
2248        })
2249    }
2250
2251    pub fn close_inactive_items_and_panes(
2252        &mut self,
2253        action: &CloseInactiveTabsAndPanes,
2254        cx: &mut ViewContext<Self>,
2255    ) {
2256        if let Some(task) =
2257            self.close_all_internal(true, action.save_intent.unwrap_or(SaveIntent::Close), cx)
2258        {
2259            task.detach_and_log_err(cx)
2260        }
2261    }
2262
2263    pub fn close_all_items_and_panes(
2264        &mut self,
2265        action: &CloseAllItemsAndPanes,
2266        cx: &mut ViewContext<Self>,
2267    ) {
2268        if let Some(task) =
2269            self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
2270        {
2271            task.detach_and_log_err(cx)
2272        }
2273    }
2274
2275    fn close_all_internal(
2276        &mut self,
2277        retain_active_pane: bool,
2278        save_intent: SaveIntent,
2279        cx: &mut ViewContext<Self>,
2280    ) -> Option<Task<Result<()>>> {
2281        let current_pane = self.active_pane();
2282
2283        let mut tasks = Vec::new();
2284
2285        if retain_active_pane {
2286            if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
2287                pane.close_inactive_items(
2288                    &CloseInactiveItems {
2289                        save_intent: None,
2290                        close_pinned: false,
2291                    },
2292                    cx,
2293                )
2294            }) {
2295                tasks.push(current_pane_close);
2296            };
2297        }
2298
2299        for pane in self.panes() {
2300            if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
2301                continue;
2302            }
2303
2304            if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
2305                pane.close_all_items(
2306                    &CloseAllItems {
2307                        save_intent: Some(save_intent),
2308                        close_pinned: false,
2309                    },
2310                    cx,
2311                )
2312            }) {
2313                tasks.push(close_pane_items)
2314            }
2315        }
2316
2317        if tasks.is_empty() {
2318            None
2319        } else {
2320            Some(cx.spawn(|_, _| async move {
2321                for task in tasks {
2322                    task.await?
2323                }
2324                Ok(())
2325            }))
2326        }
2327    }
2328
2329    pub fn is_dock_at_position_open(
2330        &self,
2331        position: DockPosition,
2332        cx: &mut ViewContext<Self>,
2333    ) -> bool {
2334        let dock = match position {
2335            DockPosition::Left => &self.left_dock,
2336            DockPosition::Bottom => &self.bottom_dock,
2337            DockPosition::Right => &self.right_dock,
2338        };
2339        dock.read(cx).is_open()
2340    }
2341
2342    pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
2343        let dock = match dock_side {
2344            DockPosition::Left => &self.left_dock,
2345            DockPosition::Bottom => &self.bottom_dock,
2346            DockPosition::Right => &self.right_dock,
2347        };
2348        let mut focus_center = false;
2349        let mut reveal_dock = false;
2350        dock.update(cx, |dock, cx| {
2351            let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
2352            let was_visible = dock.is_open() && !other_is_zoomed;
2353            dock.set_open(!was_visible, cx);
2354
2355            if dock.active_panel().is_none() && dock.panels_len() > 0 {
2356                dock.activate_panel(0, cx);
2357            }
2358
2359            if let Some(active_panel) = dock.active_panel() {
2360                if was_visible {
2361                    if active_panel.focus_handle(cx).contains_focused(cx) {
2362                        focus_center = true;
2363                    }
2364                } else {
2365                    let focus_handle = &active_panel.focus_handle(cx);
2366                    cx.focus(focus_handle);
2367                    reveal_dock = true;
2368                }
2369            }
2370        });
2371
2372        if reveal_dock {
2373            self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
2374        }
2375
2376        if focus_center {
2377            self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2378        }
2379
2380        cx.notify();
2381        self.serialize_workspace(cx);
2382    }
2383
2384    pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
2385        let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
2386
2387        for dock in docks {
2388            dock.update(cx, |dock, cx| {
2389                dock.set_open(false, cx);
2390            });
2391        }
2392
2393        cx.focus_self();
2394        cx.notify();
2395        self.serialize_workspace(cx);
2396    }
2397
2398    /// Transfer focus to the panel of the given type.
2399    pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
2400        let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
2401        panel.to_any().downcast().ok()
2402    }
2403
2404    /// Focus the panel of the given type if it isn't already focused. If it is
2405    /// already focused, then transfer focus back to the workspace center.
2406    pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2407        self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
2408            !panel.focus_handle(cx).contains_focused(cx)
2409        });
2410    }
2411
2412    pub fn activate_panel_for_proto_id(
2413        &mut self,
2414        panel_id: PanelId,
2415        cx: &mut ViewContext<Self>,
2416    ) -> Option<Arc<dyn PanelHandle>> {
2417        let mut panel = None;
2418        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2419            if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
2420                panel = dock.update(cx, |dock, cx| {
2421                    dock.activate_panel(panel_index, cx);
2422                    dock.set_open(true, cx);
2423                    dock.active_panel().cloned()
2424                });
2425                break;
2426            }
2427        }
2428
2429        if panel.is_some() {
2430            cx.notify();
2431            self.serialize_workspace(cx);
2432        }
2433
2434        panel
2435    }
2436
2437    /// Focus or unfocus the given panel type, depending on the given callback.
2438    fn focus_or_unfocus_panel<T: Panel>(
2439        &mut self,
2440        cx: &mut ViewContext<Self>,
2441        should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
2442    ) -> Option<Arc<dyn PanelHandle>> {
2443        let mut result_panel = None;
2444        let mut serialize = false;
2445        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2446            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2447                let mut focus_center = false;
2448                let panel = dock.update(cx, |dock, cx| {
2449                    dock.activate_panel(panel_index, cx);
2450
2451                    let panel = dock.active_panel().cloned();
2452                    if let Some(panel) = panel.as_ref() {
2453                        if should_focus(&**panel, cx) {
2454                            dock.set_open(true, cx);
2455                            panel.focus_handle(cx).focus(cx);
2456                        } else {
2457                            focus_center = true;
2458                        }
2459                    }
2460                    panel
2461                });
2462
2463                if focus_center {
2464                    self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2465                }
2466
2467                result_panel = panel;
2468                serialize = true;
2469                break;
2470            }
2471        }
2472
2473        if serialize {
2474            self.serialize_workspace(cx);
2475        }
2476
2477        cx.notify();
2478        result_panel
2479    }
2480
2481    /// Open the panel of the given type
2482    pub fn open_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2483        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2484            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2485                dock.update(cx, |dock, cx| {
2486                    dock.activate_panel(panel_index, cx);
2487                    dock.set_open(true, cx);
2488                });
2489            }
2490        }
2491    }
2492
2493    pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
2494        [&self.left_dock, &self.bottom_dock, &self.right_dock]
2495            .iter()
2496            .find_map(|dock| dock.read(cx).panel::<T>())
2497    }
2498
2499    fn dismiss_zoomed_items_to_reveal(
2500        &mut self,
2501        dock_to_reveal: Option<DockPosition>,
2502        cx: &mut ViewContext<Self>,
2503    ) {
2504        // If a center pane is zoomed, unzoom it.
2505        for pane in &self.panes {
2506            if pane != &self.active_pane || dock_to_reveal.is_some() {
2507                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2508            }
2509        }
2510
2511        // If another dock is zoomed, hide it.
2512        let mut focus_center = false;
2513        for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
2514            dock.update(cx, |dock, cx| {
2515                if Some(dock.position()) != dock_to_reveal {
2516                    if let Some(panel) = dock.active_panel() {
2517                        if panel.is_zoomed(cx) {
2518                            focus_center |= panel.focus_handle(cx).contains_focused(cx);
2519                            dock.set_open(false, cx);
2520                        }
2521                    }
2522                }
2523            });
2524        }
2525
2526        if focus_center {
2527            self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2528        }
2529
2530        if self.zoomed_position != dock_to_reveal {
2531            self.zoomed = None;
2532            self.zoomed_position = None;
2533            cx.emit(Event::ZoomChanged);
2534        }
2535
2536        cx.notify();
2537    }
2538
2539    fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2540        let pane = cx.new_view(|cx| {
2541            let mut pane = Pane::new(
2542                self.weak_handle(),
2543                self.project.clone(),
2544                self.pane_history_timestamp.clone(),
2545                None,
2546                NewFile.boxed_clone(),
2547                cx,
2548            );
2549            pane.set_can_split(Some(Arc::new(|_, _, _| true)));
2550            pane
2551        });
2552        cx.subscribe(&pane, Self::handle_pane_event).detach();
2553        self.panes.push(pane.clone());
2554        cx.focus_view(&pane);
2555        cx.emit(Event::PaneAdded(pane.clone()));
2556        pane
2557    }
2558
2559    pub fn add_item_to_center(
2560        &mut self,
2561        item: Box<dyn ItemHandle>,
2562        cx: &mut ViewContext<Self>,
2563    ) -> bool {
2564        if let Some(center_pane) = self.last_active_center_pane.clone() {
2565            if let Some(center_pane) = center_pane.upgrade() {
2566                center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
2567                true
2568            } else {
2569                false
2570            }
2571        } else {
2572            false
2573        }
2574    }
2575
2576    pub fn add_item_to_active_pane(
2577        &mut self,
2578        item: Box<dyn ItemHandle>,
2579        destination_index: Option<usize>,
2580        focus_item: bool,
2581        cx: &mut WindowContext,
2582    ) {
2583        self.add_item(
2584            self.active_pane.clone(),
2585            item,
2586            destination_index,
2587            false,
2588            focus_item,
2589            cx,
2590        )
2591    }
2592
2593    pub fn add_item(
2594        &mut self,
2595        pane: View<Pane>,
2596        item: Box<dyn ItemHandle>,
2597        destination_index: Option<usize>,
2598        activate_pane: bool,
2599        focus_item: bool,
2600        cx: &mut WindowContext,
2601    ) {
2602        if let Some(text) = item.telemetry_event_text(cx) {
2603            self.client()
2604                .telemetry()
2605                .report_app_event(format!("{}: open", text));
2606        }
2607
2608        pane.update(cx, |pane, cx| {
2609            pane.add_item(item, activate_pane, focus_item, destination_index, cx)
2610        });
2611    }
2612
2613    pub fn split_item(
2614        &mut self,
2615        split_direction: SplitDirection,
2616        item: Box<dyn ItemHandle>,
2617        cx: &mut ViewContext<Self>,
2618    ) {
2619        let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
2620        self.add_item(new_pane, item, None, true, true, cx);
2621    }
2622
2623    pub fn open_abs_path(
2624        &mut self,
2625        abs_path: PathBuf,
2626        visible: bool,
2627        cx: &mut ViewContext<Self>,
2628    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2629        cx.spawn(|workspace, mut cx| async move {
2630            let open_paths_task_result = workspace
2631                .update(&mut cx, |workspace, cx| {
2632                    workspace.open_paths(
2633                        vec![abs_path.clone()],
2634                        if visible {
2635                            OpenVisible::All
2636                        } else {
2637                            OpenVisible::None
2638                        },
2639                        None,
2640                        cx,
2641                    )
2642                })
2643                .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2644                .await;
2645            anyhow::ensure!(
2646                open_paths_task_result.len() == 1,
2647                "open abs path {abs_path:?} task returned incorrect number of results"
2648            );
2649            match open_paths_task_result
2650                .into_iter()
2651                .next()
2652                .expect("ensured single task result")
2653            {
2654                Some(open_result) => {
2655                    open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2656                }
2657                None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2658            }
2659        })
2660    }
2661
2662    pub fn split_abs_path(
2663        &mut self,
2664        abs_path: PathBuf,
2665        visible: bool,
2666        cx: &mut ViewContext<Self>,
2667    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2668        let project_path_task =
2669            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2670        cx.spawn(|this, mut cx| async move {
2671            let (_, path) = project_path_task.await?;
2672            this.update(&mut cx, |this, cx| this.split_path(path, cx))?
2673                .await
2674        })
2675    }
2676
2677    pub fn open_path(
2678        &mut self,
2679        path: impl Into<ProjectPath>,
2680        pane: Option<WeakView<Pane>>,
2681        focus_item: bool,
2682        cx: &mut WindowContext,
2683    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2684        self.open_path_preview(path, pane, focus_item, false, cx)
2685    }
2686
2687    pub fn open_path_preview(
2688        &mut self,
2689        path: impl Into<ProjectPath>,
2690        pane: Option<WeakView<Pane>>,
2691        focus_item: bool,
2692        allow_preview: bool,
2693        cx: &mut WindowContext,
2694    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2695        let pane = pane.unwrap_or_else(|| {
2696            self.last_active_center_pane.clone().unwrap_or_else(|| {
2697                self.panes
2698                    .first()
2699                    .expect("There must be an active pane")
2700                    .downgrade()
2701            })
2702        });
2703
2704        let task = self.load_path(path.into(), cx);
2705        cx.spawn(move |mut cx| async move {
2706            let (project_entry_id, build_item) = task.await?;
2707            pane.update(&mut cx, |pane, cx| {
2708                pane.open_item(
2709                    project_entry_id,
2710                    focus_item,
2711                    allow_preview,
2712                    None,
2713                    cx,
2714                    build_item,
2715                )
2716            })
2717        })
2718    }
2719
2720    pub fn split_path(
2721        &mut self,
2722        path: impl Into<ProjectPath>,
2723        cx: &mut ViewContext<Self>,
2724    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2725        self.split_path_preview(path, false, None, cx)
2726    }
2727
2728    pub fn split_path_preview(
2729        &mut self,
2730        path: impl Into<ProjectPath>,
2731        allow_preview: bool,
2732        split_direction: Option<SplitDirection>,
2733        cx: &mut ViewContext<Self>,
2734    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2735        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2736            self.panes
2737                .first()
2738                .expect("There must be an active pane")
2739                .downgrade()
2740        });
2741
2742        if let Member::Pane(center_pane) = &self.center.root {
2743            if center_pane.read(cx).items_len() == 0 {
2744                return self.open_path(path, Some(pane), true, cx);
2745            }
2746        }
2747
2748        let task = self.load_path(path.into(), cx);
2749        cx.spawn(|this, mut cx| async move {
2750            let (project_entry_id, build_item) = task.await?;
2751            this.update(&mut cx, move |this, cx| -> Option<_> {
2752                let pane = pane.upgrade()?;
2753                let new_pane =
2754                    this.split_pane(pane, split_direction.unwrap_or(SplitDirection::Right), cx);
2755                new_pane.update(cx, |new_pane, cx| {
2756                    Some(new_pane.open_item(
2757                        project_entry_id,
2758                        true,
2759                        allow_preview,
2760                        None,
2761                        cx,
2762                        build_item,
2763                    ))
2764                })
2765            })
2766            .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2767        })
2768    }
2769
2770    fn load_path(
2771        &mut self,
2772        path: ProjectPath,
2773        cx: &mut WindowContext,
2774    ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
2775        let project = self.project().clone();
2776        let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
2777        let Some(open_project_item) = project_item_builders
2778            .iter()
2779            .rev()
2780            .find_map(|open_project_item| open_project_item(&project, &path, cx))
2781        else {
2782            return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
2783        };
2784        open_project_item
2785    }
2786
2787    pub fn find_project_item<T>(
2788        &self,
2789        pane: &View<Pane>,
2790        project_item: &Model<T::Item>,
2791        cx: &AppContext,
2792    ) -> Option<View<T>>
2793    where
2794        T: ProjectItem,
2795    {
2796        use project::ProjectItem as _;
2797        let project_item = project_item.read(cx);
2798        let entry_id = project_item.entry_id(cx);
2799        let project_path = project_item.project_path(cx);
2800
2801        let mut item = None;
2802        if let Some(entry_id) = entry_id {
2803            item = pane.read(cx).item_for_entry(entry_id, cx);
2804        }
2805        if item.is_none() {
2806            if let Some(project_path) = project_path {
2807                item = pane.read(cx).item_for_path(project_path, cx);
2808            }
2809        }
2810
2811        item.and_then(|item| item.downcast::<T>())
2812    }
2813
2814    pub fn is_project_item_open<T>(
2815        &self,
2816        pane: &View<Pane>,
2817        project_item: &Model<T::Item>,
2818        cx: &AppContext,
2819    ) -> bool
2820    where
2821        T: ProjectItem,
2822    {
2823        self.find_project_item::<T>(pane, project_item, cx)
2824            .is_some()
2825    }
2826
2827    pub fn open_project_item<T>(
2828        &mut self,
2829        pane: View<Pane>,
2830        project_item: Model<T::Item>,
2831        activate_pane: bool,
2832        focus_item: bool,
2833        cx: &mut ViewContext<Self>,
2834    ) -> View<T>
2835    where
2836        T: ProjectItem,
2837    {
2838        if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
2839            self.activate_item(&item, activate_pane, focus_item, cx);
2840            return item;
2841        }
2842
2843        let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2844        let item_id = item.item_id();
2845        let mut destination_index = None;
2846        pane.update(cx, |pane, cx| {
2847            if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
2848                if let Some(preview_item_id) = pane.preview_item_id() {
2849                    if preview_item_id != item_id {
2850                        destination_index = pane.close_current_preview_item(cx);
2851                    }
2852                }
2853            }
2854            pane.set_preview_item_id(Some(item.item_id()), cx)
2855        });
2856
2857        self.add_item(
2858            pane,
2859            Box::new(item.clone()),
2860            destination_index,
2861            activate_pane,
2862            focus_item,
2863            cx,
2864        );
2865        item
2866    }
2867
2868    pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2869        if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2870            self.active_pane.update(cx, |pane, cx| {
2871                pane.add_item(Box::new(shared_screen), false, true, None, cx)
2872            });
2873        }
2874    }
2875
2876    pub fn activate_item(
2877        &mut self,
2878        item: &dyn ItemHandle,
2879        activate_pane: bool,
2880        focus_item: bool,
2881        cx: &mut WindowContext,
2882    ) -> bool {
2883        let result = self.panes.iter().find_map(|pane| {
2884            pane.read(cx)
2885                .index_for_item(item)
2886                .map(|ix| (pane.clone(), ix))
2887        });
2888        if let Some((pane, ix)) = result {
2889            pane.update(cx, |pane, cx| {
2890                pane.activate_item(ix, activate_pane, focus_item, cx)
2891            });
2892            true
2893        } else {
2894            false
2895        }
2896    }
2897
2898    fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2899        let panes = self.center.panes();
2900        if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2901            cx.focus_view(&pane);
2902        } else {
2903            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2904        }
2905    }
2906
2907    fn move_item_to_pane_at_index(&mut self, action: &MoveItemToPane, cx: &mut ViewContext<Self>) {
2908        let Some(&target_pane) = self.center.panes().get(action.destination) else {
2909            return;
2910        };
2911        move_active_item(&self.active_pane, target_pane, action.focus, true, cx);
2912    }
2913
2914    pub fn activate_next_pane(&mut self, cx: &mut WindowContext) {
2915        let panes = self.center.panes();
2916        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2917            let next_ix = (ix + 1) % panes.len();
2918            let next_pane = panes[next_ix].clone();
2919            cx.focus_view(&next_pane);
2920        }
2921    }
2922
2923    pub fn activate_previous_pane(&mut self, cx: &mut WindowContext) {
2924        let panes = self.center.panes();
2925        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2926            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2927            let prev_pane = panes[prev_ix].clone();
2928            cx.focus_view(&prev_pane);
2929        }
2930    }
2931
2932    pub fn activate_pane_in_direction(
2933        &mut self,
2934        direction: SplitDirection,
2935        cx: &mut WindowContext,
2936    ) {
2937        use ActivateInDirectionTarget as Target;
2938        enum Origin {
2939            LeftDock,
2940            RightDock,
2941            BottomDock,
2942            Center,
2943        }
2944
2945        let origin: Origin = [
2946            (&self.left_dock, Origin::LeftDock),
2947            (&self.right_dock, Origin::RightDock),
2948            (&self.bottom_dock, Origin::BottomDock),
2949        ]
2950        .into_iter()
2951        .find_map(|(dock, origin)| {
2952            if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
2953                Some(origin)
2954            } else {
2955                None
2956            }
2957        })
2958        .unwrap_or(Origin::Center);
2959
2960        let get_last_active_pane = || {
2961            let pane = self
2962                .last_active_center_pane
2963                .clone()
2964                .unwrap_or_else(|| {
2965                    self.panes
2966                        .first()
2967                        .expect("There must be an active pane")
2968                        .downgrade()
2969                })
2970                .upgrade()?;
2971            (pane.read(cx).items_len() != 0).then_some(pane)
2972        };
2973
2974        let try_dock =
2975            |dock: &View<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
2976
2977        let target = match (origin, direction) {
2978            // We're in the center, so we first try to go to a different pane,
2979            // otherwise try to go to a dock.
2980            (Origin::Center, direction) => {
2981                if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2982                    Some(Target::Pane(pane))
2983                } else {
2984                    match direction {
2985                        SplitDirection::Up => None,
2986                        SplitDirection::Down => try_dock(&self.bottom_dock),
2987                        SplitDirection::Left => try_dock(&self.left_dock),
2988                        SplitDirection::Right => try_dock(&self.right_dock),
2989                    }
2990                }
2991            }
2992
2993            (Origin::LeftDock, SplitDirection::Right) => {
2994                if let Some(last_active_pane) = get_last_active_pane() {
2995                    Some(Target::Pane(last_active_pane))
2996                } else {
2997                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
2998                }
2999            }
3000
3001            (Origin::LeftDock, SplitDirection::Down)
3002            | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
3003
3004            (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
3005            (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
3006            (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
3007
3008            (Origin::RightDock, SplitDirection::Left) => {
3009                if let Some(last_active_pane) = get_last_active_pane() {
3010                    Some(Target::Pane(last_active_pane))
3011                } else {
3012                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
3013                }
3014            }
3015
3016            _ => None,
3017        };
3018
3019        match target {
3020            Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
3021            Some(ActivateInDirectionTarget::Dock(dock)) => {
3022                // Defer this to avoid a panic when the dock's active panel is already on the stack.
3023                cx.defer(move |cx| {
3024                    let dock = dock.read(cx);
3025                    if let Some(panel) = dock.active_panel() {
3026                        panel.focus_handle(cx).focus(cx);
3027                    } else {
3028                        log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
3029                    }
3030                })
3031            }
3032            None => {}
3033        }
3034    }
3035
3036    pub fn move_item_to_pane_in_direction(
3037        &mut self,
3038        action: &MoveItemToPaneInDirection,
3039        cx: &mut WindowContext,
3040    ) {
3041        if let Some(destination) = self.find_pane_in_direction(action.direction, cx) {
3042            move_active_item(&self.active_pane, &destination, action.focus, true, cx);
3043        }
3044    }
3045
3046    pub fn bounding_box_for_pane(&self, pane: &View<Pane>) -> Option<Bounds<Pixels>> {
3047        self.center.bounding_box_for_pane(pane)
3048    }
3049
3050    pub fn find_pane_in_direction(
3051        &mut self,
3052        direction: SplitDirection,
3053        cx: &WindowContext,
3054    ) -> Option<View<Pane>> {
3055        self.center
3056            .find_pane_in_direction(&self.active_pane, direction, cx)
3057            .cloned()
3058    }
3059
3060    pub fn swap_pane_in_direction(
3061        &mut self,
3062        direction: SplitDirection,
3063        cx: &mut ViewContext<Self>,
3064    ) {
3065        if let Some(to) = self.find_pane_in_direction(direction, cx) {
3066            self.center.swap(&self.active_pane, &to);
3067            cx.notify();
3068        }
3069    }
3070
3071    pub fn resize_pane(&mut self, axis: gpui::Axis, amount: Pixels, cx: &mut ViewContext<Self>) {
3072        self.center
3073            .resize(&self.active_pane, axis, amount, &self.bounds);
3074        cx.notify();
3075    }
3076
3077    pub fn reset_pane_sizes(&mut self, cx: &mut ViewContext<Self>) {
3078        self.center.reset_pane_sizes();
3079        cx.notify();
3080    }
3081
3082    fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3083        // This is explicitly hoisted out of the following check for pane identity as
3084        // terminal panel panes are not registered as a center panes.
3085        self.status_bar.update(cx, |status_bar, cx| {
3086            status_bar.set_active_pane(&pane, cx);
3087        });
3088        if self.active_pane != pane {
3089            self.set_active_pane(&pane, cx);
3090        }
3091
3092        if self.last_active_center_pane.is_none() {
3093            self.last_active_center_pane = Some(pane.downgrade());
3094        }
3095
3096        self.dismiss_zoomed_items_to_reveal(None, cx);
3097        if pane.read(cx).is_zoomed() {
3098            self.zoomed = Some(pane.downgrade().into());
3099        } else {
3100            self.zoomed = None;
3101        }
3102        self.zoomed_position = None;
3103        cx.emit(Event::ZoomChanged);
3104        self.update_active_view_for_followers(cx);
3105        pane.model.update(cx, |pane, _| {
3106            pane.track_alternate_file_items();
3107        });
3108
3109        cx.notify();
3110    }
3111
3112    fn set_active_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) {
3113        self.active_pane = pane.clone();
3114        self.active_item_path_changed(cx);
3115        self.last_active_center_pane = Some(pane.downgrade());
3116    }
3117
3118    fn handle_panel_focused(&mut self, cx: &mut ViewContext<Self>) {
3119        self.update_active_view_for_followers(cx);
3120    }
3121
3122    fn handle_pane_event(
3123        &mut self,
3124        pane: View<Pane>,
3125        event: &pane::Event,
3126        cx: &mut ViewContext<Self>,
3127    ) {
3128        let mut serialize_workspace = true;
3129        match event {
3130            pane::Event::AddItem { item } => {
3131                item.added_to_pane(self, pane, cx);
3132                cx.emit(Event::ItemAdded {
3133                    item: item.boxed_clone(),
3134                });
3135            }
3136            pane::Event::Split(direction) => {
3137                self.split_and_clone(pane, *direction, cx);
3138            }
3139            pane::Event::JoinIntoNext => {
3140                self.join_pane_into_next(pane, cx);
3141            }
3142            pane::Event::JoinAll => {
3143                self.join_all_panes(cx);
3144            }
3145            pane::Event::Remove { focus_on_pane } => {
3146                self.remove_pane(pane, focus_on_pane.clone(), cx);
3147            }
3148            pane::Event::ActivateItem {
3149                local,
3150                focus_changed,
3151            } => {
3152                cx.on_next_frame(|_, cx| {
3153                    cx.invalidate_character_coordinates();
3154                });
3155
3156                pane.model.update(cx, |pane, _| {
3157                    pane.track_alternate_file_items();
3158                });
3159                if *local {
3160                    self.unfollow_in_pane(&pane, cx);
3161                }
3162                if &pane == self.active_pane() {
3163                    self.active_item_path_changed(cx);
3164                    self.update_active_view_for_followers(cx);
3165                }
3166                serialize_workspace = *focus_changed || &pane != self.active_pane();
3167            }
3168            pane::Event::UserSavedItem { item, save_intent } => {
3169                cx.emit(Event::UserSavedItem {
3170                    pane: pane.downgrade(),
3171                    item: item.boxed_clone(),
3172                    save_intent: *save_intent,
3173                });
3174                serialize_workspace = false;
3175            }
3176            pane::Event::ChangeItemTitle => {
3177                if pane == self.active_pane {
3178                    self.active_item_path_changed(cx);
3179                }
3180                self.update_window_edited(cx);
3181                serialize_workspace = false;
3182            }
3183            pane::Event::RemoveItem { .. } => {}
3184            pane::Event::RemovedItem { item_id } => {
3185                cx.emit(Event::ActiveItemChanged);
3186                self.update_window_edited(cx);
3187                if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
3188                    if entry.get().entity_id() == pane.entity_id() {
3189                        entry.remove();
3190                    }
3191                }
3192            }
3193            pane::Event::Focus => {
3194                cx.on_next_frame(|_, cx| {
3195                    cx.invalidate_character_coordinates();
3196                });
3197                self.handle_pane_focused(pane.clone(), cx);
3198            }
3199            pane::Event::ZoomIn => {
3200                if pane == self.active_pane {
3201                    pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3202                    if pane.read(cx).has_focus(cx) {
3203                        self.zoomed = Some(pane.downgrade().into());
3204                        self.zoomed_position = None;
3205                        cx.emit(Event::ZoomChanged);
3206                    }
3207                    cx.notify();
3208                }
3209            }
3210            pane::Event::ZoomOut => {
3211                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3212                if self.zoomed_position.is_none() {
3213                    self.zoomed = None;
3214                    cx.emit(Event::ZoomChanged);
3215                }
3216                cx.notify();
3217            }
3218        }
3219
3220        if serialize_workspace {
3221            self.serialize_workspace(cx);
3222        }
3223    }
3224
3225    pub fn unfollow_in_pane(
3226        &mut self,
3227        pane: &View<Pane>,
3228        cx: &mut ViewContext<Workspace>,
3229    ) -> Option<PeerId> {
3230        let leader_id = self.leader_for_pane(pane)?;
3231        self.unfollow(leader_id, cx);
3232        Some(leader_id)
3233    }
3234
3235    pub fn split_pane(
3236        &mut self,
3237        pane_to_split: View<Pane>,
3238        split_direction: SplitDirection,
3239        cx: &mut ViewContext<Self>,
3240    ) -> View<Pane> {
3241        let new_pane = self.add_pane(cx);
3242        self.center
3243            .split(&pane_to_split, &new_pane, split_direction)
3244            .unwrap();
3245        cx.notify();
3246        new_pane
3247    }
3248
3249    pub fn split_and_clone(
3250        &mut self,
3251        pane: View<Pane>,
3252        direction: SplitDirection,
3253        cx: &mut ViewContext<Self>,
3254    ) -> Option<View<Pane>> {
3255        let item = pane.read(cx).active_item()?;
3256        let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
3257            let new_pane = self.add_pane(cx);
3258            new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
3259            self.center.split(&pane, &new_pane, direction).unwrap();
3260            Some(new_pane)
3261        } else {
3262            None
3263        };
3264        cx.notify();
3265        maybe_pane_handle
3266    }
3267
3268    pub fn split_pane_with_item(
3269        &mut self,
3270        pane_to_split: WeakView<Pane>,
3271        split_direction: SplitDirection,
3272        from: WeakView<Pane>,
3273        item_id_to_move: EntityId,
3274        cx: &mut ViewContext<Self>,
3275    ) {
3276        let Some(pane_to_split) = pane_to_split.upgrade() else {
3277            return;
3278        };
3279        let Some(from) = from.upgrade() else {
3280            return;
3281        };
3282
3283        let new_pane = self.add_pane(cx);
3284        move_item(&from, &new_pane, item_id_to_move, 0, cx);
3285        self.center
3286            .split(&pane_to_split, &new_pane, split_direction)
3287            .unwrap();
3288        cx.notify();
3289    }
3290
3291    pub fn split_pane_with_project_entry(
3292        &mut self,
3293        pane_to_split: WeakView<Pane>,
3294        split_direction: SplitDirection,
3295        project_entry: ProjectEntryId,
3296        cx: &mut ViewContext<Self>,
3297    ) -> Option<Task<Result<()>>> {
3298        let pane_to_split = pane_to_split.upgrade()?;
3299        let new_pane = self.add_pane(cx);
3300        self.center
3301            .split(&pane_to_split, &new_pane, split_direction)
3302            .unwrap();
3303
3304        let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3305        let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
3306        Some(cx.foreground_executor().spawn(async move {
3307            task.await?;
3308            Ok(())
3309        }))
3310    }
3311
3312    pub fn join_all_panes(&mut self, cx: &mut ViewContext<Self>) {
3313        let active_item = self.active_pane.read(cx).active_item();
3314        for pane in &self.panes {
3315            join_pane_into_active(&self.active_pane, pane, cx);
3316        }
3317        if let Some(active_item) = active_item {
3318            self.activate_item(active_item.as_ref(), true, true, cx);
3319        }
3320        cx.notify();
3321    }
3322
3323    pub fn join_pane_into_next(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3324        let next_pane = self
3325            .find_pane_in_direction(SplitDirection::Right, cx)
3326            .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3327            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3328            .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3329        let Some(next_pane) = next_pane else {
3330            return;
3331        };
3332        move_all_items(&pane, &next_pane, cx);
3333        cx.notify();
3334    }
3335
3336    fn remove_pane(
3337        &mut self,
3338        pane: View<Pane>,
3339        focus_on: Option<View<Pane>>,
3340        cx: &mut ViewContext<Self>,
3341    ) {
3342        if self.center.remove(&pane).unwrap() {
3343            self.force_remove_pane(&pane, &focus_on, cx);
3344            self.unfollow_in_pane(&pane, cx);
3345            self.last_leaders_by_pane.remove(&pane.downgrade());
3346            for removed_item in pane.read(cx).items() {
3347                self.panes_by_item.remove(&removed_item.item_id());
3348            }
3349
3350            cx.notify();
3351        } else {
3352            self.active_item_path_changed(cx);
3353        }
3354        cx.emit(Event::PaneRemoved);
3355    }
3356
3357    pub fn panes(&self) -> &[View<Pane>] {
3358        &self.panes
3359    }
3360
3361    pub fn active_pane(&self) -> &View<Pane> {
3362        &self.active_pane
3363    }
3364
3365    pub fn focused_pane(&self, cx: &WindowContext) -> View<Pane> {
3366        for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3367            if dock.focus_handle(cx).contains_focused(cx) {
3368                if let Some(pane) = dock
3369                    .read(cx)
3370                    .active_panel()
3371                    .and_then(|panel| panel.pane(cx))
3372                {
3373                    return pane;
3374                }
3375            }
3376        }
3377        self.active_pane().clone()
3378    }
3379
3380    pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
3381        self.find_pane_in_direction(SplitDirection::Right, cx)
3382            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3383            .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
3384            .clone()
3385    }
3386
3387    pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
3388        let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3389        weak_pane.upgrade()
3390    }
3391
3392    fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
3393        self.follower_states.retain(|leader_id, state| {
3394            if *leader_id == peer_id {
3395                for item in state.items_by_leader_view_id.values() {
3396                    item.view.set_leader_peer_id(None, cx);
3397                }
3398                false
3399            } else {
3400                true
3401            }
3402        });
3403        cx.notify();
3404    }
3405
3406    pub fn start_following(
3407        &mut self,
3408        leader_id: PeerId,
3409        cx: &mut ViewContext<Self>,
3410    ) -> Option<Task<Result<()>>> {
3411        let pane = self.active_pane().clone();
3412
3413        self.last_leaders_by_pane
3414            .insert(pane.downgrade(), leader_id);
3415        self.unfollow(leader_id, cx);
3416        self.unfollow_in_pane(&pane, cx);
3417        self.follower_states.insert(
3418            leader_id,
3419            FollowerState {
3420                center_pane: pane.clone(),
3421                dock_pane: None,
3422                active_view_id: None,
3423                items_by_leader_view_id: Default::default(),
3424            },
3425        );
3426        cx.notify();
3427
3428        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3429        let project_id = self.project.read(cx).remote_id();
3430        let request = self.app_state.client.request(proto::Follow {
3431            room_id,
3432            project_id,
3433            leader_id: Some(leader_id),
3434        });
3435
3436        Some(cx.spawn(|this, mut cx| async move {
3437            let response = request.await?;
3438            this.update(&mut cx, |this, _| {
3439                let state = this
3440                    .follower_states
3441                    .get_mut(&leader_id)
3442                    .ok_or_else(|| anyhow!("following interrupted"))?;
3443                state.active_view_id = response
3444                    .active_view
3445                    .as_ref()
3446                    .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3447                Ok::<_, anyhow::Error>(())
3448            })??;
3449            if let Some(view) = response.active_view {
3450                Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
3451            }
3452            this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
3453            Ok(())
3454        }))
3455    }
3456
3457    pub fn follow_next_collaborator(
3458        &mut self,
3459        _: &FollowNextCollaborator,
3460        cx: &mut ViewContext<Self>,
3461    ) {
3462        let collaborators = self.project.read(cx).collaborators();
3463        let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3464            let mut collaborators = collaborators.keys().copied();
3465            for peer_id in collaborators.by_ref() {
3466                if peer_id == leader_id {
3467                    break;
3468                }
3469            }
3470            collaborators.next()
3471        } else if let Some(last_leader_id) =
3472            self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3473        {
3474            if collaborators.contains_key(last_leader_id) {
3475                Some(*last_leader_id)
3476            } else {
3477                None
3478            }
3479        } else {
3480            None
3481        };
3482
3483        let pane = self.active_pane.clone();
3484        let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3485        else {
3486            return;
3487        };
3488        if self.unfollow_in_pane(&pane, cx) == Some(leader_id) {
3489            return;
3490        }
3491        if let Some(task) = self.start_following(leader_id, cx) {
3492            task.detach_and_log_err(cx)
3493        }
3494    }
3495
3496    pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
3497        let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3498            return;
3499        };
3500        let room = room.read(cx);
3501        let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3502            return;
3503        };
3504
3505        let project = self.project.read(cx);
3506
3507        let other_project_id = match remote_participant.location {
3508            call::ParticipantLocation::External => None,
3509            call::ParticipantLocation::UnsharedProject => None,
3510            call::ParticipantLocation::SharedProject { project_id } => {
3511                if Some(project_id) == project.remote_id() {
3512                    None
3513                } else {
3514                    Some(project_id)
3515                }
3516            }
3517        };
3518
3519        // if they are active in another project, follow there.
3520        if let Some(project_id) = other_project_id {
3521            let app_state = self.app_state.clone();
3522            crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3523                .detach_and_log_err(cx);
3524        }
3525
3526        // if you're already following, find the right pane and focus it.
3527        if let Some(follower_state) = self.follower_states.get(&leader_id) {
3528            cx.focus_view(follower_state.pane());
3529            return;
3530        }
3531
3532        // Otherwise, follow.
3533        if let Some(task) = self.start_following(leader_id, cx) {
3534            task.detach_and_log_err(cx)
3535        }
3536    }
3537
3538    pub fn unfollow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3539        cx.notify();
3540        let state = self.follower_states.remove(&leader_id)?;
3541        for (_, item) in state.items_by_leader_view_id {
3542            item.view.set_leader_peer_id(None, cx);
3543        }
3544
3545        let project_id = self.project.read(cx).remote_id();
3546        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3547        self.app_state
3548            .client
3549            .send(proto::Unfollow {
3550                room_id,
3551                project_id,
3552                leader_id: Some(leader_id),
3553            })
3554            .log_err();
3555
3556        Some(())
3557    }
3558
3559    pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3560        self.follower_states.contains_key(&peer_id)
3561    }
3562
3563    fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
3564        cx.emit(Event::ActiveItemChanged);
3565        let active_entry = self.active_project_path(cx);
3566        self.project
3567            .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3568
3569        self.update_window_title(cx);
3570    }
3571
3572    fn update_window_title(&mut self, cx: &mut WindowContext) {
3573        let project = self.project().read(cx);
3574        let mut title = String::new();
3575
3576        for (i, name) in project.worktree_root_names(cx).enumerate() {
3577            if i > 0 {
3578                title.push_str(", ");
3579            }
3580            title.push_str(name);
3581        }
3582
3583        if title.is_empty() {
3584            title = "empty project".to_string();
3585        }
3586
3587        if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3588            let filename = path
3589                .path
3590                .file_name()
3591                .map(|s| s.to_string_lossy())
3592                .or_else(|| {
3593                    Some(Cow::Borrowed(
3594                        project
3595                            .worktree_for_id(path.worktree_id, cx)?
3596                            .read(cx)
3597                            .root_name(),
3598                    ))
3599                });
3600
3601            if let Some(filename) = filename {
3602                title.push_str("");
3603                title.push_str(filename.as_ref());
3604            }
3605        }
3606
3607        if project.is_via_collab() {
3608            title.push_str("");
3609        } else if project.is_shared() {
3610            title.push_str("");
3611        }
3612
3613        cx.set_window_title(&title);
3614    }
3615
3616    fn update_window_edited(&mut self, cx: &mut WindowContext) {
3617        let is_edited = !self.project.read(cx).is_disconnected(cx)
3618            && self
3619                .items(cx)
3620                .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
3621        if is_edited != self.window_edited {
3622            self.window_edited = is_edited;
3623            cx.set_window_edited(self.window_edited)
3624        }
3625    }
3626
3627    fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
3628        if self.notifications.is_empty() {
3629            None
3630        } else {
3631            Some(
3632                div()
3633                    .absolute()
3634                    .right_3()
3635                    .bottom_3()
3636                    .w_112()
3637                    .h_full()
3638                    .flex()
3639                    .flex_col()
3640                    .justify_end()
3641                    .gap_2()
3642                    .children(
3643                        self.notifications
3644                            .iter()
3645                            .map(|(_, notification)| notification.clone().into_any()),
3646                    ),
3647            )
3648        }
3649    }
3650
3651    // RPC handlers
3652
3653    fn active_view_for_follower(
3654        &self,
3655        follower_project_id: Option<u64>,
3656        cx: &mut ViewContext<Self>,
3657    ) -> Option<proto::View> {
3658        let (item, panel_id) = self.active_item_for_followers(cx);
3659        let item = item?;
3660        let leader_id = self
3661            .pane_for(&*item)
3662            .and_then(|pane| self.leader_for_pane(&pane));
3663
3664        let item_handle = item.to_followable_item_handle(cx)?;
3665        let id = item_handle.remote_id(&self.app_state.client, cx)?;
3666        let variant = item_handle.to_state_proto(cx)?;
3667
3668        if item_handle.is_project_item(cx)
3669            && (follower_project_id.is_none()
3670                || follower_project_id != self.project.read(cx).remote_id())
3671        {
3672            return None;
3673        }
3674
3675        Some(proto::View {
3676            id: Some(id.to_proto()),
3677            leader_id,
3678            variant: Some(variant),
3679            panel_id: panel_id.map(|id| id as i32),
3680        })
3681    }
3682
3683    fn handle_follow(
3684        &mut self,
3685        follower_project_id: Option<u64>,
3686        cx: &mut ViewContext<Self>,
3687    ) -> proto::FollowResponse {
3688        let active_view = self.active_view_for_follower(follower_project_id, cx);
3689
3690        cx.notify();
3691        proto::FollowResponse {
3692            // TODO: Remove after version 0.145.x stabilizes.
3693            active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3694            views: active_view.iter().cloned().collect(),
3695            active_view,
3696        }
3697    }
3698
3699    fn handle_update_followers(
3700        &mut self,
3701        leader_id: PeerId,
3702        message: proto::UpdateFollowers,
3703        _cx: &mut ViewContext<Self>,
3704    ) {
3705        self.leader_updates_tx
3706            .unbounded_send((leader_id, message))
3707            .ok();
3708    }
3709
3710    async fn process_leader_update(
3711        this: &WeakView<Self>,
3712        leader_id: PeerId,
3713        update: proto::UpdateFollowers,
3714        cx: &mut AsyncWindowContext,
3715    ) -> Result<()> {
3716        match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3717            proto::update_followers::Variant::CreateView(view) => {
3718                let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3719                let should_add_view = this.update(cx, |this, _| {
3720                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
3721                        anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
3722                    } else {
3723                        anyhow::Ok(false)
3724                    }
3725                })??;
3726
3727                if should_add_view {
3728                    Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3729                }
3730            }
3731            proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3732                let should_add_view = this.update(cx, |this, _| {
3733                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
3734                        state.active_view_id = update_active_view
3735                            .view
3736                            .as_ref()
3737                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3738
3739                        if state.active_view_id.is_some_and(|view_id| {
3740                            !state.items_by_leader_view_id.contains_key(&view_id)
3741                        }) {
3742                            anyhow::Ok(true)
3743                        } else {
3744                            anyhow::Ok(false)
3745                        }
3746                    } else {
3747                        anyhow::Ok(false)
3748                    }
3749                })??;
3750
3751                if should_add_view {
3752                    if let Some(view) = update_active_view.view {
3753                        Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3754                    }
3755                }
3756            }
3757            proto::update_followers::Variant::UpdateView(update_view) => {
3758                let variant = update_view
3759                    .variant
3760                    .ok_or_else(|| anyhow!("missing update view variant"))?;
3761                let id = update_view
3762                    .id
3763                    .ok_or_else(|| anyhow!("missing update view id"))?;
3764                let mut tasks = Vec::new();
3765                this.update(cx, |this, cx| {
3766                    let project = this.project.clone();
3767                    if let Some(state) = this.follower_states.get(&leader_id) {
3768                        let view_id = ViewId::from_proto(id.clone())?;
3769                        if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
3770                            tasks.push(item.view.apply_update_proto(&project, variant.clone(), cx));
3771                        }
3772                    }
3773                    anyhow::Ok(())
3774                })??;
3775                try_join_all(tasks).await.log_err();
3776            }
3777        }
3778        this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
3779        Ok(())
3780    }
3781
3782    async fn add_view_from_leader(
3783        this: WeakView<Self>,
3784        leader_id: PeerId,
3785        view: &proto::View,
3786        cx: &mut AsyncWindowContext,
3787    ) -> Result<()> {
3788        let this = this.upgrade().context("workspace dropped")?;
3789
3790        let Some(id) = view.id.clone() else {
3791            return Err(anyhow!("no id for view"));
3792        };
3793        let id = ViewId::from_proto(id)?;
3794        let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
3795
3796        let pane = this.update(cx, |this, _cx| {
3797            let state = this
3798                .follower_states
3799                .get(&leader_id)
3800                .context("stopped following")?;
3801            anyhow::Ok(state.pane().clone())
3802        })??;
3803        let existing_item = pane.update(cx, |pane, cx| {
3804            let client = this.read(cx).client().clone();
3805            pane.items().find_map(|item| {
3806                let item = item.to_followable_item_handle(cx)?;
3807                if item.remote_id(&client, cx) == Some(id) {
3808                    Some(item)
3809                } else {
3810                    None
3811                }
3812            })
3813        })?;
3814        let item = if let Some(existing_item) = existing_item {
3815            existing_item
3816        } else {
3817            let variant = view.variant.clone();
3818            if variant.is_none() {
3819                Err(anyhow!("missing view variant"))?;
3820            }
3821
3822            let task = cx.update(|cx| {
3823                FollowableViewRegistry::from_state_proto(this.clone(), id, variant, cx)
3824            })?;
3825
3826            let Some(task) = task else {
3827                return Err(anyhow!(
3828                    "failed to construct view from leader (maybe from a different version of zed?)"
3829                ));
3830            };
3831
3832            let mut new_item = task.await?;
3833            pane.update(cx, |pane, cx| {
3834                let mut item_to_remove = None;
3835                for (ix, item) in pane.items().enumerate() {
3836                    if let Some(item) = item.to_followable_item_handle(cx) {
3837                        match new_item.dedup(item.as_ref(), cx) {
3838                            Some(item::Dedup::KeepExisting) => {
3839                                new_item =
3840                                    item.boxed_clone().to_followable_item_handle(cx).unwrap();
3841                                break;
3842                            }
3843                            Some(item::Dedup::ReplaceExisting) => {
3844                                item_to_remove = Some((ix, item.item_id()));
3845                                break;
3846                            }
3847                            None => {}
3848                        }
3849                    }
3850                }
3851
3852                if let Some((ix, id)) = item_to_remove {
3853                    pane.remove_item(id, false, false, cx);
3854                    pane.add_item(new_item.boxed_clone(), false, false, Some(ix), cx);
3855                }
3856            })?;
3857
3858            new_item
3859        };
3860
3861        this.update(cx, |this, cx| {
3862            let state = this.follower_states.get_mut(&leader_id)?;
3863            item.set_leader_peer_id(Some(leader_id), cx);
3864            state.items_by_leader_view_id.insert(
3865                id,
3866                FollowerView {
3867                    view: item,
3868                    location: panel_id,
3869                },
3870            );
3871
3872            Some(())
3873        })?;
3874
3875        Ok(())
3876    }
3877
3878    pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3879        let mut is_project_item = true;
3880        let mut update = proto::UpdateActiveView::default();
3881        if cx.is_window_active() {
3882            let (active_item, panel_id) = self.active_item_for_followers(cx);
3883
3884            if let Some(item) = active_item {
3885                if item.focus_handle(cx).contains_focused(cx) {
3886                    let leader_id = self
3887                        .pane_for(&*item)
3888                        .and_then(|pane| self.leader_for_pane(&pane));
3889
3890                    if let Some(item) = item.to_followable_item_handle(cx) {
3891                        let id = item
3892                            .remote_id(&self.app_state.client, cx)
3893                            .map(|id| id.to_proto());
3894
3895                        if let Some(id) = id.clone() {
3896                            if let Some(variant) = item.to_state_proto(cx) {
3897                                let view = Some(proto::View {
3898                                    id: Some(id.clone()),
3899                                    leader_id,
3900                                    variant: Some(variant),
3901                                    panel_id: panel_id.map(|id| id as i32),
3902                                });
3903
3904                                is_project_item = item.is_project_item(cx);
3905                                update = proto::UpdateActiveView {
3906                                    view,
3907                                    // TODO: Remove after version 0.145.x stabilizes.
3908                                    id: Some(id.clone()),
3909                                    leader_id,
3910                                };
3911                            }
3912                        };
3913                    }
3914                }
3915            }
3916        }
3917
3918        let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
3919        if active_view_id != self.last_active_view_id.as_ref() {
3920            self.last_active_view_id = active_view_id.cloned();
3921            self.update_followers(
3922                is_project_item,
3923                proto::update_followers::Variant::UpdateActiveView(update),
3924                cx,
3925            );
3926        }
3927    }
3928
3929    fn active_item_for_followers(
3930        &self,
3931        cx: &mut WindowContext,
3932    ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
3933        let mut active_item = None;
3934        let mut panel_id = None;
3935        for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3936            if dock.focus_handle(cx).contains_focused(cx) {
3937                if let Some(panel) = dock.read(cx).active_panel() {
3938                    if let Some(pane) = panel.pane(cx) {
3939                        if let Some(item) = pane.read(cx).active_item() {
3940                            active_item = Some(item);
3941                            panel_id = panel.remote_id();
3942                            break;
3943                        }
3944                    }
3945                }
3946            }
3947        }
3948
3949        if active_item.is_none() {
3950            active_item = self.active_pane().read(cx).active_item();
3951        }
3952        (active_item, panel_id)
3953    }
3954
3955    fn update_followers(
3956        &self,
3957        project_only: bool,
3958        update: proto::update_followers::Variant,
3959        cx: &mut WindowContext,
3960    ) -> Option<()> {
3961        // If this update only applies to for followers in the current project,
3962        // then skip it unless this project is shared. If it applies to all
3963        // followers, regardless of project, then set `project_id` to none,
3964        // indicating that it goes to all followers.
3965        let project_id = if project_only {
3966            Some(self.project.read(cx).remote_id()?)
3967        } else {
3968            None
3969        };
3970        self.app_state().workspace_store.update(cx, |store, cx| {
3971            store.update_followers(project_id, update, cx)
3972        })
3973    }
3974
3975    pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3976        self.follower_states.iter().find_map(|(leader_id, state)| {
3977            if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
3978                Some(*leader_id)
3979            } else {
3980                None
3981            }
3982        })
3983    }
3984
3985    fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3986        cx.notify();
3987
3988        let call = self.active_call()?;
3989        let room = call.read(cx).room()?.read(cx);
3990        let participant = room.remote_participant_for_peer_id(leader_id)?;
3991
3992        let leader_in_this_app;
3993        let leader_in_this_project;
3994        match participant.location {
3995            call::ParticipantLocation::SharedProject { project_id } => {
3996                leader_in_this_app = true;
3997                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3998            }
3999            call::ParticipantLocation::UnsharedProject => {
4000                leader_in_this_app = true;
4001                leader_in_this_project = false;
4002            }
4003            call::ParticipantLocation::External => {
4004                leader_in_this_app = false;
4005                leader_in_this_project = false;
4006            }
4007        };
4008
4009        let state = self.follower_states.get(&leader_id)?;
4010        let mut item_to_activate = None;
4011        if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
4012            if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
4013                if leader_in_this_project || !item.view.is_project_item(cx) {
4014                    item_to_activate = Some((item.location, item.view.boxed_clone()));
4015                }
4016            }
4017        } else if let Some(shared_screen) =
4018            self.shared_screen_for_peer(leader_id, &state.center_pane, cx)
4019        {
4020            item_to_activate = Some((None, Box::new(shared_screen)));
4021        }
4022
4023        let (panel_id, item) = item_to_activate?;
4024
4025        let mut transfer_focus = state.center_pane.read(cx).has_focus(cx);
4026        let pane;
4027        if let Some(panel_id) = panel_id {
4028            pane = self.activate_panel_for_proto_id(panel_id, cx)?.pane(cx)?;
4029            let state = self.follower_states.get_mut(&leader_id)?;
4030            state.dock_pane = Some(pane.clone());
4031        } else {
4032            pane = state.center_pane.clone();
4033            let state = self.follower_states.get_mut(&leader_id)?;
4034            if let Some(dock_pane) = state.dock_pane.take() {
4035                transfer_focus |= dock_pane.focus_handle(cx).contains_focused(cx);
4036            }
4037        }
4038
4039        pane.update(cx, |pane, cx| {
4040            let focus_active_item = pane.has_focus(cx) || transfer_focus;
4041            if let Some(index) = pane.index_for_item(item.as_ref()) {
4042                pane.activate_item(index, false, false, cx);
4043            } else {
4044                pane.add_item(item.boxed_clone(), false, false, None, cx)
4045            }
4046
4047            if focus_active_item {
4048                pane.focus_active_item(cx)
4049            }
4050        });
4051
4052        None
4053    }
4054
4055    #[cfg(target_os = "windows")]
4056    fn shared_screen_for_peer(
4057        &self,
4058        _peer_id: PeerId,
4059        _pane: &View<Pane>,
4060        _cx: &mut WindowContext,
4061    ) -> Option<View<SharedScreen>> {
4062        None
4063    }
4064
4065    #[cfg(not(target_os = "windows"))]
4066    fn shared_screen_for_peer(
4067        &self,
4068        peer_id: PeerId,
4069        pane: &View<Pane>,
4070        cx: &mut WindowContext,
4071    ) -> Option<View<SharedScreen>> {
4072        let call = self.active_call()?;
4073        let room = call.read(cx).room()?.read(cx);
4074        let participant = room.remote_participant_for_peer_id(peer_id)?;
4075        let track = participant.video_tracks.values().next()?.clone();
4076        let user = participant.user.clone();
4077
4078        for item in pane.read(cx).items_of_type::<SharedScreen>() {
4079            if item.read(cx).peer_id == peer_id {
4080                return Some(item);
4081            }
4082        }
4083
4084        Some(cx.new_view(|cx| SharedScreen::new(track, peer_id, user.clone(), cx)))
4085    }
4086
4087    pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
4088        if cx.is_window_active() {
4089            self.update_active_view_for_followers(cx);
4090
4091            if let Some(database_id) = self.database_id {
4092                cx.background_executor()
4093                    .spawn(persistence::DB.update_timestamp(database_id))
4094                    .detach();
4095            }
4096        } else {
4097            for pane in &self.panes {
4098                pane.update(cx, |pane, cx| {
4099                    if let Some(item) = pane.active_item() {
4100                        item.workspace_deactivated(cx);
4101                    }
4102                    for item in pane.items() {
4103                        if matches!(
4104                            item.workspace_settings(cx).autosave,
4105                            AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4106                        ) {
4107                            Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
4108                                .detach_and_log_err(cx);
4109                        }
4110                    }
4111                });
4112            }
4113        }
4114    }
4115
4116    pub fn active_call(&self) -> Option<&Model<ActiveCall>> {
4117        self.active_call.as_ref().map(|(call, _)| call)
4118    }
4119
4120    fn on_active_call_event(
4121        &mut self,
4122        _: Model<ActiveCall>,
4123        event: &call::room::Event,
4124        cx: &mut ViewContext<Self>,
4125    ) {
4126        match event {
4127            call::room::Event::ParticipantLocationChanged { participant_id }
4128            | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4129                self.leader_updated(*participant_id, cx);
4130            }
4131            _ => {}
4132        }
4133    }
4134
4135    pub fn database_id(&self) -> Option<WorkspaceId> {
4136        self.database_id
4137    }
4138
4139    fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
4140        let project = self.project().read(cx);
4141
4142        if project.is_local() {
4143            Some(
4144                project
4145                    .visible_worktrees(cx)
4146                    .map(|worktree| worktree.read(cx).abs_path())
4147                    .collect::<Vec<_>>(),
4148            )
4149        } else {
4150            None
4151        }
4152    }
4153
4154    fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
4155        match member {
4156            Member::Axis(PaneAxis { members, .. }) => {
4157                for child in members.iter() {
4158                    self.remove_panes(child.clone(), cx)
4159                }
4160            }
4161            Member::Pane(pane) => {
4162                self.force_remove_pane(&pane, &None, cx);
4163            }
4164        }
4165    }
4166
4167    fn remove_from_session(&mut self, cx: &mut WindowContext) -> Task<()> {
4168        self.session_id.take();
4169        self.serialize_workspace_internal(cx)
4170    }
4171
4172    fn force_remove_pane(
4173        &mut self,
4174        pane: &View<Pane>,
4175        focus_on: &Option<View<Pane>>,
4176        cx: &mut ViewContext<Workspace>,
4177    ) {
4178        self.panes.retain(|p| p != pane);
4179        if let Some(focus_on) = focus_on {
4180            focus_on.update(cx, |pane, cx| pane.focus(cx));
4181        } else {
4182            self.panes
4183                .last()
4184                .unwrap()
4185                .update(cx, |pane, cx| pane.focus(cx));
4186        }
4187        if self.last_active_center_pane == Some(pane.downgrade()) {
4188            self.last_active_center_pane = None;
4189        }
4190        cx.notify();
4191    }
4192
4193    fn serialize_workspace(&mut self, cx: &mut ViewContext<Self>) {
4194        if self._schedule_serialize.is_none() {
4195            self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
4196                cx.background_executor()
4197                    .timer(Duration::from_millis(100))
4198                    .await;
4199                this.update(&mut cx, |this, cx| {
4200                    this.serialize_workspace_internal(cx).detach();
4201                    this._schedule_serialize.take();
4202                })
4203                .log_err();
4204            }));
4205        }
4206    }
4207
4208    fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
4209        let Some(database_id) = self.database_id() else {
4210            return Task::ready(());
4211        };
4212
4213        fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
4214            let (items, active, pinned_count) = {
4215                let pane = pane_handle.read(cx);
4216                let active_item_id = pane.active_item().map(|item| item.item_id());
4217                (
4218                    pane.items()
4219                        .filter_map(|handle| {
4220                            let handle = handle.to_serializable_item_handle(cx)?;
4221
4222                            Some(SerializedItem {
4223                                kind: Arc::from(handle.serialized_item_kind()),
4224                                item_id: handle.item_id().as_u64(),
4225                                active: Some(handle.item_id()) == active_item_id,
4226                                preview: pane.is_active_preview_item(handle.item_id()),
4227                            })
4228                        })
4229                        .collect::<Vec<_>>(),
4230                    pane.has_focus(cx),
4231                    pane.pinned_count(),
4232                )
4233            };
4234
4235            SerializedPane::new(items, active, pinned_count)
4236        }
4237
4238        fn build_serialized_pane_group(
4239            pane_group: &Member,
4240            cx: &WindowContext,
4241        ) -> SerializedPaneGroup {
4242            match pane_group {
4243                Member::Axis(PaneAxis {
4244                    axis,
4245                    members,
4246                    flexes,
4247                    bounding_boxes: _,
4248                }) => SerializedPaneGroup::Group {
4249                    axis: SerializedAxis(*axis),
4250                    children: members
4251                        .iter()
4252                        .map(|member| build_serialized_pane_group(member, cx))
4253                        .collect::<Vec<_>>(),
4254                    flexes: Some(flexes.lock().clone()),
4255                },
4256                Member::Pane(pane_handle) => {
4257                    SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
4258                }
4259            }
4260        }
4261
4262        fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
4263            let left_dock = this.left_dock.read(cx);
4264            let left_visible = left_dock.is_open();
4265            let left_active_panel = left_dock
4266                .active_panel()
4267                .map(|panel| panel.persistent_name().to_string());
4268            let left_dock_zoom = left_dock
4269                .active_panel()
4270                .map(|panel| panel.is_zoomed(cx))
4271                .unwrap_or(false);
4272
4273            let right_dock = this.right_dock.read(cx);
4274            let right_visible = right_dock.is_open();
4275            let right_active_panel = right_dock
4276                .active_panel()
4277                .map(|panel| panel.persistent_name().to_string());
4278            let right_dock_zoom = right_dock
4279                .active_panel()
4280                .map(|panel| panel.is_zoomed(cx))
4281                .unwrap_or(false);
4282
4283            let bottom_dock = this.bottom_dock.read(cx);
4284            let bottom_visible = bottom_dock.is_open();
4285            let bottom_active_panel = bottom_dock
4286                .active_panel()
4287                .map(|panel| panel.persistent_name().to_string());
4288            let bottom_dock_zoom = bottom_dock
4289                .active_panel()
4290                .map(|panel| panel.is_zoomed(cx))
4291                .unwrap_or(false);
4292
4293            DockStructure {
4294                left: DockData {
4295                    visible: left_visible,
4296                    active_panel: left_active_panel,
4297                    zoom: left_dock_zoom,
4298                },
4299                right: DockData {
4300                    visible: right_visible,
4301                    active_panel: right_active_panel,
4302                    zoom: right_dock_zoom,
4303                },
4304                bottom: DockData {
4305                    visible: bottom_visible,
4306                    active_panel: bottom_active_panel,
4307                    zoom: bottom_dock_zoom,
4308                },
4309            }
4310        }
4311
4312        let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4313            Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4314        } else if let Some(local_paths) = self.local_paths(cx) {
4315            if !local_paths.is_empty() {
4316                Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4317            } else {
4318                None
4319            }
4320        } else {
4321            None
4322        };
4323
4324        if let Some(location) = location {
4325            let center_group = build_serialized_pane_group(&self.center.root, cx);
4326            let docks = build_serialized_docks(self, cx);
4327            let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
4328            let serialized_workspace = SerializedWorkspace {
4329                id: database_id,
4330                location,
4331                center_group,
4332                window_bounds,
4333                display: Default::default(),
4334                docks,
4335                centered_layout: self.centered_layout,
4336                session_id: self.session_id.clone(),
4337                window_id: Some(cx.window_handle().window_id().as_u64()),
4338            };
4339            return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
4340        }
4341        Task::ready(())
4342    }
4343
4344    async fn serialize_items(
4345        this: &WeakView<Self>,
4346        items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4347        cx: &mut AsyncWindowContext,
4348    ) -> Result<()> {
4349        const CHUNK_SIZE: usize = 200;
4350        const THROTTLE_TIME: Duration = Duration::from_millis(200);
4351
4352        let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4353
4354        while let Some(items_received) = serializable_items.next().await {
4355            let unique_items =
4356                items_received
4357                    .into_iter()
4358                    .fold(HashMap::default(), |mut acc, item| {
4359                        acc.entry(item.item_id()).or_insert(item);
4360                        acc
4361                    });
4362
4363            // We use into_iter() here so that the references to the items are moved into
4364            // the tasks and not kept alive while we're sleeping.
4365            for (_, item) in unique_items.into_iter() {
4366                if let Ok(Some(task)) =
4367                    this.update(cx, |workspace, cx| item.serialize(workspace, false, cx))
4368                {
4369                    cx.background_executor()
4370                        .spawn(async move { task.await.log_err() })
4371                        .detach();
4372                }
4373            }
4374
4375            cx.background_executor().timer(THROTTLE_TIME).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}