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